0

When I'm writing unit-tests, I generally try to pre-empt mistakes I (or perhaps another developer) is likely to come along and make.

Take this class Foo for example, which has an equality-operator:

struct Foo {
  int m_foo;

  bool operator==(const Foo& other) const {
    return m_foo == other.m_foo;
  }
};

// test code
TEST(FooEquality, Equal) {
  Foo f1, f2;
  f1.m_foo = 1;
  f2.m_foo = 1;

  EXPECT_EQ(f1, f2);
}

TEST(FooEquality, NotEqual) {
  Foo f1, f2;
  f1.m_foo = 1
  f2.m_foo = 2;

  EXPECT_NE(f1, f2);
}

A simple but likely problem: future me adds a new member variable m_bar and forgets to update the equality operator.

struct Foo {
  int m_foo;
  int m_bar;

  bool operator==(const Foo& other) const {
    // BUG: We didn't check m_bar, but tests still pass!
    return m_foo == other.m_foo;
  }
};

Is there a clean way of writing a test for this?

arrtchiu
  • 1,034
  • 1
  • 8
  • 23
  • The test code still compiles because it accesses internal details directly to initialize instances. If there was access control and a constructor, which was changed with the addition of the new member, the test code would fail to compile and would have to be updated. So just don't do the dirty. – Cheers and hth. - Alf Apr 12 '18 at 04:17
  • You should implement operators like `==` as non-member functions to have more encapsulation and allow implicit conversions on both sides. – BessieTheCookie Apr 12 '18 at 04:32
  • In your example you can simply call default constructor, like `Foo f1 = {1}, f2 = {2}`, so compilation of test cases will fail once you add any more members and, therefore, change the default constructor. – Denis Sheremet Apr 12 '18 at 05:13
  • Generally, according to TDD principles, you should not add anything to production code before you have a test for it. It may be easier to comply to TDD rather than using compiler to force it for us. – Yksisarvinen Apr 12 '18 at 07:26

1 Answers1

1

Is there a clean way of writing a test for this?

Not in the usual sense, no.

Which is to say, the unit tests under discussion in TDD are expressions of a specification of a contract; they don't constrain the implementation, just the observable results.

Which means if you make a change in your implementation that is backwards compatible with the previous API, then the unit tests are going to observed equivalent behaviors.

Unit tests are not supposed to be inspecting the internals of the implementation.

If you review Beck's Test Driven Development By Example, you'll observe that has has a running to-do list, which he is using to keep track of tests that he anticipates needing. He adds to this list when his recognition of a particular pattern helps him to identify a new test to add to the suite.

But the actual trigger is his experience as a developer. So the new tests for the assignment operator are supposed to come from your recognition that "oh, I've change the internal representation of state, do I need to add any new tests?" in much the same way that writing one of the big three five.

That said, you might be able to achieve the results you want via static analysis. The rough sketch of the idea being that the analysis tool can look at the class members, and the assignment operator, and not that there are missing members which have not been explicitly excused.

There's a closed question here on stack overflow that can get you started with some of the options.

Disclaimer: it's been years since I've played in the C++ sandbox, I don't know what is covered by today's analysis tools. I merely recommend that as a possible starting point.

VoiceOfUnreason
  • 40,245
  • 4
  • 34
  • 73