10

Sometimes objects just need to be tightly coupled. For example, a CsvFile class will probably need to work tightly with the CsvRecord class (or ICsvRecord interface).

However from what I learned in the past, one of test-driven development's main tenets is "Never test more than one class at a time." Meaning you should use ICsvRecord mocks or stubs rather than actual instances of CsvRecord.

However after trying this approach, I noticed that mocking out the CsvRecord class can get a little hairy. Which leads me to one of two conclusions:

  1. It's hard to write unit tests! That's a code smell! Refactor!
  2. Mocking out every single dependency is just unreasonable.

When I replaced my mocks with actual CsvRecord instances, things went much more smoothly. When looking around for other peoples' thoughts I stumbled across this blog post, which seems to support #2 above. For objects that are naturally tightly coupled, we should not worry so much about mocking.

Am I way off track? Are there any downsides to assumption #2 above? Should I actually be thinking about refactoring my design?

1
  • 1
    I think it is a common misconception that the "unit" in "unit tests" must necessarily be one class. I think your example shows a case where it may be better that those two classes form one unit. But don't get me wrong, I agree totally with Robert Harvey's answer.
    – Doc Brown
    Commented Aug 11, 2012 at 10:26

5 Answers 5

11

If you really need coordination between those two classes, write a CsvCoordinator class that encapsulates your two classes, and test that.

However, I dispute the notion that CsvRecord is not independently testable. CsvRecord is basically a DTO class, is it not? It's just a collection of fields, with maybe a couple of helper methods. And CsvRecord can be used in other contexts besides CsvFile; you can have a collection or array of CsvRecords, for example.

Test CsvRecord first. Make sure that it passes all of its tests. Then, go ahead and use CsvRecord with your CsvFile class during test. Use it as a pre-tested stub/mock; fill it with relevant test data, pass it to CsvFile, and write your test cases against that.

4
  • 1
    Yes, CsvRecord is most definitely independently testable. The problem is that if something breaks in CsvRecord, it will cause CsvData tests to fail. But I don't think that's a major issue.
    – Phil
    Commented Aug 11, 2012 at 2:34
  • 1
    I think you want that to happen. :) Commented Aug 11, 2012 at 3:05
  • 1
    @RobertHarvey: in theory, it could become a problem if CsvRecord and CsvFile are becoming quite complex classes, and if a test breaks for CsvFile, now you don't know immediatly if it is a problem in CsvFile or CsvRecord. But I guess that's more a hypothetical case - if I had the task of programming such classes for a real-world program, I would do it exactly the way you describe it.
    – Doc Brown
    Commented Aug 11, 2012 at 10:35
  • 2
    @Phil: If CsvRecord breaks, then obviously CsvData fails; but this is OK, because you test CsvRecord first, and if that fails, your CsvFile tests are meaningless. You can still distinguish between errors in CsvRecord and in CsvFile.
    – tdammers
    Commented Aug 11, 2012 at 11:59
5

The reason for testing one class at a time is that you don't want tests for one class to have dependencies on the behaviour of a second class. That means that if your test for Class A exercises any of the functionality of Class B, then you should mock Class B to remove the dependency on particular functionality within Class B.

A class like CsvRecord seems to me that it's mostly for storing data - it's not a class with too much functionality of its own. That is, it may have constructors, getters, setters, but no methods with real substantial logic. Of course, I'm guessing here - maybe you've written a class called CsvRecord that does numerous complex calculations.

But if CsvRecord doesn't have real logic of its own, there's nothing to be gained by mocking it. This is really just the old maxim - "don't mock value objects" .

So when considering whether to mock a particular class (for a test of a different class), you should take into account how much of its own logic that class has, and how much of that logic will be executed in the course of your test.

1
  • +1. Any test whose outcome depends on the correctness of more than one object's behavior is an integration test, not a unit test. You have to mock out one of these objects to get a real unit test. This doesn't apply to objects with no real behavior in them though -with only getters and setters for instance. Commented Aug 11, 2012 at 13:15
1

No. #2 is fine. Things can be, and should be tightly coupled if their concepts are tightly coupled. This should be rare, and generally avoided, but in the example you provided it makes sense.

0

"Coupled" classes are mutually dependent on one another. This should not be the case in what you are describing--a CsvRecord should not really care about the CsvFile containing it, so the dependency only goes one way. That's fine, and is not tight coupling.

After all, if a class contains the variable String name, you would not claim it is tightly coupled to String, would you?

So, unit test the CsvRecord for its desired behavior.

Then use a mocking framework (Mockito is great) to test if your unit is interacting with the objects it depends on correctly. The behavior you want to test, really--is the CsvFile handles CsvRcords in the expected fashion. The inner workings of CvsRecord shouldn't matter--it's how CvsFile communicates with it.

Finally, TDD is not only about unit tests. You certainly can (and should) start with functional tests that look at the functional behavior of how your larger components work--that is, your user story or scenario. Your units tests set the expectations and verify the pieces, the functional tests do the same for the whole.

2
  • 1
    -1, tight coupling does not necessarily mean cyclic dependencies, that's a misconception. In the example, CsvFile is tightly coupled to CsvRecord (but not the other way round). The OP asks if is a good a idea to test CsvFile by decoupling it from CsvRecord via an ICsvRecord, not vice versa.
    – Doc Brown
    Commented Aug 11, 2012 at 10:23
  • 2
    @DocBrown: Whether the coupling is tight or not is a matter of how much CsvFile depends on the inner workings of CsvRecord, that is, the amount of assumptions that the file has about the record. Interfaces help document and enforce such assumptions (or rather the absence of other assumptions), but the amount of coupling remains the same, except that with an interface, you can hook a different record class into CsvFile. Introducing the interface just so you can say you have reduced coupling is silly.
    – tdammers
    Commented Aug 11, 2012 at 12:03
0

There are really two questions here. The first is if situations exist where mocking an object is inadvisable. That's undoubtedly true, as shown by the other excellent answers. The second question is whether your particular case is one of those situations. On that question I am not convinced.

Probably the most common reason not to mock a class is if it's a value class. However, you have to look at the reason behind the rule. It's not because the mocked class will be bad somehow, it's because it will be essentially identical to the original. If that were the case, your unit testing would not be any easier using the original class.

It very well may be that your code is one of the rare exceptions where refactoring wouldn't help, but you should only declare it such after diligent refactoring efforts haven't worked. Even seasoned developers can have trouble seeing alternatives to their own design. If you can't think of any possible way to improve it, ask someone experienced to give it a second look.

Most people seem to be assuming your CsvRecord is a value class. Try making it one. Make it immutable if you can. If you have two objects with pointers to each other, remove one of them and figure out how to make it work. Look for places to split classes and functions. The best place to split a class may not always match up with the physical layout of the file. Try reversing the parent/child relationship of the classes. Maybe you need a separate class for reading and writing csv files. Maybe you need separate classes for handling the file I/O and the interface to the upper layers. There are lots of things to try before declaring it unrefactorable.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.