Custom Assertion

Implementation of a custom assertion function.

Full source code of the example

Imagine a situation when you want to compare two files. First one is generated by a tested code, the second one contains an expected output. To reach this goal one can write a helper function

bool compareFiles(
    std::istream& generated_,
    std::istream& expected_) {
  /* ...... */
}

and then use it in his test.

  testAssert(compareFiles(generated_, expected_));

If the assertion fails similar output will be shown

 ============================== CustomAssertion ===============================
[.../examples/assertion/filecompare.ot2:43] assertion::CustomAssertion::FileComp
are: 'compareFiles(generated_, expected_)' has failed
  FileCompare                                                         [Failed]
 ------------------------------------------------------------------------------
  Suite total                                                         [Failed]
 ================================ Test results ================================
                      Passed              Failed               Total
  Suites                   0                   1                   1
  Cases                    0                   1                   1
  Checks                   0                   1                   1
  Errors                                                           0
  Test total                                                          [Failed]
 ==============================================================================

The test correctly fails but no one knows what has actually happened. The OTest2 framework offers two ways how to solve this problem:

  • the AssertBean
  • implementation of custom assertion function.

The first option is a simple class containing a boolean flag and a message. The helper function may return the bean. The testAssert() function is overloaded for and if the flag is false the message is reported.

The second option is more complex but it offers more flexibility.

Implementation of a custom assertion consists of two parts:

  • an implementation class,
  • a declaration.

Firstly, we’ll create the implementation class.

#include <otest2/assertcontext.h>

class FileCompare : public AssertContext {
  public:
    /* -- inherit the constructor - the parent constructor is invoked
     *    from the generated code. */
    using AssertContext::AssertContext;

    bool testCompareFiles(
        std::istream& file_,
        std::istream& expected_);
};

The implementation class must derive from the AssertContext class and it must inherit its constructor method. The testCompareFiles implements the comparison logic which has been implemented in the compareFiles function.

bool FileCompare::testCompareFiles(
    std::istream& file_,
    std::istream& expected_) {
  /* -- read contents of the files */
  std::vector<std::string> current_data_;
  slurpFile(current_data_, file_);
  std::vector<std::string> expected_data_;
  slurpFile(expected_data_, expected_);

  /* -- compute the difference */
  DiffLogBlocks diff_log_;
  DiffLogBuilderBlock log_builder_(&diff_log_);
  hirschbergDiff(current_data_, expected_data_, log_builder_);

  bool result_(diff_log_.empty());
  AssertStream report_(enterAssertion(result_));
  if(result_) {
    /* -- There is no difference, the files match */
    report_ << "OK" << commitMsg();
  }
  else {
    /* -- the files are different */
    report_ << "the file is different than the expected one" << commitMsg();

    /* -- print the difference */
    for(const auto& difference_ : diff_log_) {
      for(int i_(difference_.left_begin); i_ < difference_.left_end; ++i_) {
        report_ << foreground(Color::GREEN) << std::setfill('0')
            << std::setw(4) << (i_ + 1) << "     : + " << current_data_[i_]
            << resetAttrs() << commitMsg();
      }

      for(int i_(difference_.right_begin); i_ < difference_.right_end; ++i_) {
        report_ << foreground(Color::RED) << std::setfill('0')
            << "     " << std::setw(4) << (i_ + 1) << ": - " << expected_data_[i_]
            << resetAttrs() << commitMsg();
      }
    }
  }
  return report_.getResult();
}

The function firstly reads contents of the files and then computes their difference. If the difference is not empty the assertion fails and the difference is reported.

The function enterAssertion creates an assertion stream which must be used for composing of assertion messages. The implementor of the custom assertion function is responsible to print at least one message.

Lifetime of the assertion stream defines time for what the reporters keep the assertion opened. At the time the stream is destroyed the reporters finish the opened assertion (e.g. creates a record in an XML tree).

The assertion stream implements standard std::ostream interface so everything you know about streams may be used for formatting of assertion messages.

In addition, the assertion stream offers several manipulators. The most important one is the ::OTest2::commitMessage() which marks end of currently composing assertion message. There are some other manipulators allowing to change text color ::OTest2::foreground() and ::OTest2::background() or text style ::OTest2::textStyle() or printing the stringifized assertion parameter ::OTest2::assertPar().

Now it’s time to declare the assertion function.

#include <otest2/assertionannotation.h>

bool testFileCompare(
    std::istream& file_,
    std::istream& expected_)
TEST_ASSERTION_MARK(::OTest2::Examples::FileCompare, testCompareFiles);

The assertion function is declared as any function. The only difference is the annotation TEST_ASSERTION_MARK which links the function to its implementation class. As the member function of the implementation class must be specified, the implementation class may define several assertions.

It’s a good manner to return result flag from the assertion function (true if the assertion passes). The OTest2 framework doesn’t break running tests if an assertion fails. Hence, sometimes it’s needed to take some checks conditionaly:

  if(testAssertNotEqual(pointer_, nullptr)) {
    testAssertEqual(pointer_->getName(), "...");
  }

If the assertion function is a function template, a special annotation TEST_ASSERTION_MARK_TMPL can be used. A part of the annotation may be a sequence $n or ${n} which expands to a deduced type of the n-th template argument (1 based index). Only template type parameters and template template parameters are supported. See how the relational assertions are implemented.

Now if we change the test

TEST_SUITE(CustomAssertion) {
  TEST_CASE(FileCompare) {
    TEST_SIMPLE() {
      std::istringstream current_("Hello world");
      std::istringstream expected_("Hello world!\nHave a nice day.");
      testFileCompare(current_, expected_);
    }
  }
}

we’ll get nice descriptive output:

 ============================== CustomAssertion ===============================
[.../otest2/examples/assertion/filecompare.ot2:41] assertion::CustomAssertion::F
ileCompare: the file is different than the expected one
    0001     : + Hello world
         0001: - Hello world!
         0002: - Have a nice day.
  FileCompare                                                         [Failed]
 ------------------------------------------------------------------------------
  Suite total                                                         [Failed]
 ================================ Test results ================================
                      Passed              Failed               Total
  Suites                   0                   1                   1
  Cases                    0                   1                   1
  Checks                   0                   1                   1
  Errors                                                           0
  Test total                                                          [Failed]
 ==============================================================================