tags:

views:

265

answers:

5

I have finally got in my mind what worried me about Dependency Injection and similar techniques that should make unit tests easier. Let's take this example:

public interface IRepository { void Item Find(); a lot of other methods here; }
[Test]
public void Test()
{
  var repository = Mock<IRepository>();
  repository.Expect(x => x.Find());
  var service = new Service(repository);
  service.ProcessWithItem();
}

Now, what's wrong with the code above? It's that our test roughly peeks into ProcessWithItem() implementation. What if it wants to do "from x in GetAll() where x..." - but no, our test knows what is going to happen there. And that's just a simple example. Imaging few calls that our test now is tied with, and when we want to change from GetAll() to a better GetAllFastWithoutStuff() inside the method... our test(s) are broken. Please change them. A lot of crappy work that happens so often without any real need.

And that's what often makes me to stop write tests. I just don't see how I can test without knowing implementation details. And knowing them, tests are now very fragile and pain to do.

Sure, it's not about interface (or DI) only. POCOs (and POJOs, why not) also suffer from the same thing, but they're now tied with the data, not with the interface. But the principle is the same - our final assertion is tightly coupled with our knowledge of what our SUT is going to do. "Yes you HAVE to provide this field, sir, and this better be of this value".

As a consequence, tests ARE going to fail - soon and often. This is pain. And the problem.

Are there any techniques to deal with this? AutoMockingContainer (which basically takes care all ALL methods and nested DI hierarchies) looks promising, but with its own drawback. Anything else?

A: 

Remember when you're writing a test you're not testing your repository, you're testing your Service class. In this specific example ProcessWithItem method. You create your expectations for repository object. By the way, you forgot to specify expected return for your x.Find method. That's the beauty of DI that you isolate everything from the code you about to write (I assume you do TDD).

To be honest I cannot relate to the problem you describe.

Vadim
The syntax is fake, never mind. You say "I create expectations" while in fact "I provide implementation". And it is not "expectations for repository", it is "expectations for what service is going to call". Both of these means that I actually test with implementation in mind. Which is white-box testing. And as such it creates a lot of troubles in tests once you change what is inside the white-box (the service implementation). Any single change in the impl. can cause many tests to fail. And this is white-box testing, whether it's good, bad, or "as is".
queen3
I still have hard time to relate to your problem. If an application is successful it always changes. And as developers we change / refactor our code constantly. The reason we use DI to isolated the dependencies, so we don't need to change a lot when something else is changed. Make sure that your unit-test tests only one thing.
Vadim
The problem is: implementation is going to change. Ergo, tests are going to change. In short, unit testing is always white-box and as such is a big pain to maintain. Yes you test only one thing, but from many angles (input is null, is wrong, throws, etc). So even for one thing you're going to have many tests. And they're all going to fail - soon and often. This is pain. And the problem.
queen3
And the more fluid your design the more unit testing code you'll throw away. It can be pretty damn depressing.
Will
Exactly, I sometimes even think that TDD is for the waterflow approach with full-blown architecture designed first... Well, maybe there's a balance here, that design should be done to some extent... and if you throw away too much test code then you better go to the blackboard again ;-)
queen3
A: 

Yeah, that's one of the big problems with unit testing. That, and refactoring. And design changes that are a regular occurrence with Agile. And the inexperience of those creating the tests. And etc etc...

I think the only thing the average non-critical-systems developer can do is pick and choose your battles wisely. Early in development identify the truly critical paths and test those. Weigh the likelihood of that code changing before spending lots of time testing the rest of it.

If anybody figures it all out please let us know.

Will
-1 for an unsubstantiated critique of a discipline without answering the question.
TrueWill
-1 for pretending to be the real Will when we all know you're the FAKE WILL. PRETENDER!
Will
To answer the answer, I can't say that I agree with it, but it reflects the way I do today - choose areas to test, etc. Though this doesn't really answer my question. But I don't see any critique there, and I rarely see much sense in answers' rating anyway.
queen3
There really isn't any "answer" to this problem. Maybe you should wiki? The idea isn't to find the perfect answer, its to find strategies to mitigate the common downfalls of unit testing. The second paragraph is pretty much what I try to do. I also fail miserably at it, hence my third paragraph.
Will
You're right, and that's what I asked about - techniques to deal with the problem, not a single great answer. One thing I learned already is that I better avoid "mocks duplication" - i.e. if I mock Find() in every test, it's a problem; if I move it to smth like ObjectMother with GetMockRepositoryForServiceTests() - I need to change only one place to fix all the Service tests. DRY is once again here. That's what I really try to find out here - some experience from those who did already fight against the beast ;-)
queen3
+3  A: 

Dependency Injection, per se, would let you inject an implementation of IRepository that accepts whatever calls are made on it, checks that the invariants and preconditions are satisfied, and returns results satisfying the postconditions. When you choose to inject a mock object that has very specific expectations for what methods will be called, then yes, you're doing highly implementation-specific testing -- but Dependency Injection is totally innocent in the matter, since it never dictates WHAT you should inject; rather, your beef appears to be with Mocking -- in fact, specifically the somewhat-automated mocking approach that you have chosen to use, which is one based on very specific expectations.

Mocking with very specific expectations IS indeed useful for white-box testing only. Depending on the tools / frameworks / libraries you're using (and you're not even specifying the exact programming language in a tag, so I assume your question is totally open ended) you may be able to specify the degrees of freedom allowed (these calls are allowed to come in any orders, these arguments must only satisfy the following preconditions, etc, etc). However, I don't know of an automated tool to perform exactly what you need for opaque-box testing, which is the "generic, tolerant implementation of yonder interface with all the ''programming by contract'' checks that are needed and no other".

What I tend to do over the life of a project is to build up a library of "not quite mocks" for the major interfaces needed. In some cases those may be somewhat obvious from the start, but in other cases they emerge incrementally as I'm considering some major refactoring, as follows (typical scenario)...:

The early stages of the refactoring break some aspect of the fragile strong-expectations mocking that I have cheaply put in place initially, I ponder whether to just tweak the expectations or go whole hog, if I decide it's not a one-off (i.e. the return in future refactorings and tests will justify the investment) then I hand-code a good "not quite mock" and stash it away in the project's specific bag of tricks -- actually often reusable across projects; such classes/packages as MockFilesystem, MockBigtable, MockDom, MockHttpClient, MockHttpServer, etc etc, go into a project-agnostic repository and get reused for testing all kinds of future projects (and in fact may be shared with other teams across the company, if several teams are using filesystem interfaces, bigtable interfaces, DOMs, http client/server interfaces, etc etc, that are uniform across the teams).

I acknowledge that the use of the word "mock" may be slightly inappropriate here if you take "mock" to refer specifically to the precise-expectation style of "fake implementation for testing purposes" of interfaces. Maybe Stub, Shim, Fake, Test, or some other prefix yet might be preferable (I do tend to use Mock for historical reasons, except when I remember to specifically call it Fake or the like;-).

If I was using languages with clear and precise way to express in the language itself the various design-by-contract specs in an interface, I imagine I'd get automatic tool support for most of this faking/shimming/etc; however I mostly code in other languages so I have to do a bit more manual work here. But I think that's a separate issue.

Alex Martelli
Hm... not that I got everything you said. One idea I got from it, is that smth. like Object Mother is better... because of duplication of mocks is eliminated. Still it's white-box testing. Or, can you show how do you design and test the method above - ProcessWithItem() - so that you don't need to know about the Find() call and how it is expected to behave?
queen3
+1 good analysis, concerning DI.
KLE
@queen3, I can't show how I'd implement a FakeRepository implementation of IRepository without (a) fixing the language (these days I program almost exclusively in Python, C++, SQL and Javascript, btw), (b) seeing the design-by-contract specs for it. OF COURSE I have to know how e.g. Find is expected to behave: meaning, invariants, pre-conditions, post-conditions -- how's ANYbody supposed to guess at THOSE?! At least in comments/docstrings if the language doesn't directly support DbC! What's THAT gotta do with whiteboxes?! @KLE, thanks, it's appreciated.
Alex Martelli
Saying that it's language's fault doesn't make it easier to fix unit tests every time Service implementation changes. And I don't ask to implement FakeRepository. Am I not SO clear about what I say? I ask to implement test for Service.ProcessWithItem() so that I don't have to change the test if ProcessWithItem() implementation decides to use another IRepository method.
queen3
BTW, even though I don't really like the answer but it made me think about DRY regarding mocks. Thus +1.
queen3
@queen3, **the** (one and only) way that you'll **never** have to change the test (as the code-under-test changes its use of IRepository) **is** to implement FakeRepository -- every method, every pre/post dbc condition. Don't you see that? If you omit or skimp on a method or any legit way to call it, the code-under-test COULD change tomorrow to use that one method in that one way you're not covering -- and boom, you then do have to change the test. If there are method in IRepo that the code-under-test must NEVER EVER call... then the methods should NOT be in the interface, btw!-).
Alex Martelli
So to expand on the last observation: for example if IRepository has methods such as DeleteItem, DestroyRepo, etc, and the code-under-test is supposed to use the repo in a read-only manner, then the code-under-test should receive an implementation of IRepoRO, a subset interface (with IRepository extending IRepoRO only for cases where access is not R/O). This is good practice quite apart from testing issues (a generalization of design by weakest precondition).
Alex Martelli
+1  A: 

I read the excellent book http://www.manning.com/rainsberger/. I would like to provide some insight I gained from it. I believe several advice could help you to reduce the coupling between your tests and your implementation.

Edited: included in this coupling is the test asserting that the code under test calls some methods. Calling some method is never a functional need, it is an implementation concern. It relates to an interface other than the one being tested.

  1. In many cases, the testing should be about the external behavior of an interface, and be completely black-box testing them.

    The author gives the example that the test classes should be in a different package than the class to test. At first, I was sure this was wrong, because it makes it more difficult to test protected and package methods. But he argues that you should only test the external behavior of a system, that is the public methods. The non-public methods are implementation-details, and testing it results in coupling the test with the implementation. This was very insightful to me.

    By the way, this book has so many excellent practical advice on how to design tests (say JUnit tests), that I would buy it on my own money if it wasn't provided by the company! ;-)

  2. An excellent other advice from the book was to test at the functionality level, not the method level. For example, testing the add() method for a list requires trusted size() and get() methods, but they in turn require add() so we have a loop, we can't test safely. But testing the list's behavior globally (accross all methods) when adding involves testing the three methods at the same time, not proving that each is correct in isolation, but checking that together they provide the expected behavior. Often, when you try to test one of your methods in isolation, you cannot write a sensible test without using other methods, so you end up testing the implementation instead ; the consequence are coupling between test and implementation.
    Only test functionalities, not methods.

  3. Also, note that testing using external ressources (the database being the more common, but many others exist) is much slower, requires some access (IP, licence etc) from the executing machine, require a started container, may be sensitive to simultaneous access (a database can't run reliably multiple JUnit campaign at the same time), and has many other drawbacks. If all your tests use external resources, then you are in trouble, you can't run all your tests all the time, from any machine, from many machines at once, etc. So I understood (still from the book):

    Test only once each external resource (database for example), in a dedicated test that is not a unit-test, but an integration test (although it can still use the same JUnit technology if appropriate).

    Test enough dedicated tests to trust the resource is working. Then, other tests should never test it again, this is a waste, they should trust it.

    Note that the current Maven best-practices give similar advice (see free book "Better builds with Maven"). I believe this is not a coincidence:

    • The JUnits in the test directory of a project are real unit tests. They run every time you do something with your project (except just compile).
    • The integration and functional tests should be provided in a different project, an integration-test project. They only run in a much later (optional) phase, after you have deployed your whole application in the container.
KLE
Sorry but I don't see how this is related to the question... #1 and #3 are already done in the example; #2 I don't really understand how it solves white-box about interfaces injected.
queen3
Sorry, I probably wrote poorly, so you didn't understand. Well, I try again: the example relies on the implementation (what methods does the tested code call?), while #1-2-3 explain how to do useful testing without using implementation. Black-box testing (#1) does not rely on any implementation calls. For #3, we would need to consider all your test to decide on it, but if you say so, I agree. #2 is about the test granularity, it also helps us to write useful test with no reference to implementation.
KLE
+1  A: 

As a consequence, tests ARE going to fail - soon and often. This is pain. And the problem.

Well yes, unit tests can depend on internal implementation details. And sure, such "white box" tests are more brittle than "black box" tests which only rely on the externally published contract.

But I don't agree that this has to cause regular test failures. Think about how you arrived at testing with mocks in the first place: you've used dependency injection to limit the responsibilities of the class, to decrease coupling to other code, and to enable testing the class in isolation.

Are there any techniques to deal with this?

A good unit test can only fail if you change the class under test, even if it depends on internal implementation details. And you can limit the responsibilities and coupling (to other classes) of your class, so that you will rarely have to change it.

In practice you'll have to be pragmatic; every now and then you'll write "unit tests" that are actually integration tests involving multiple classes or over-sized classes. Brittle tests depending on internal implementation details are more dangerous in that case. But for truly TDD-style classes, not so much.

Wim Coenen
Can you show me an example of a "black-box" unit test? Even simple Assert.That(new Item().Count, Is.EqualTo(0)) DOES peek inside Item's constructor.
queen3
@queen3: the example you give is a black box test. The documentation of that constructor will state that Count is 0 after creation. It's a post condition, and as such part of the externally visible contract of the code. It is not an internal implementation detail.
Wim Coenen
I actually asked about the code example in the question. But if we talk about this little code line, let's count possible changes that will cause pain: 1) Count changes to GetCount() 2) Removal of Count 3) "Documentation" will state that Count = 1 after creation (no compiler support) 4) Item lose default constructor 5) Item become internal to some other class or removed... Now, can you count how many tests do already assume both "new Item()" and "new Item().Count == 0" and how many of them are to be changed?
queen3
@queen3: yes, changes in the externally exposed part of a class will cause pain in any code that uses the class. This has nothing to do with unit tests in particular.
Wim Coenen
@queen3: ah now I see the source of confusion: for you, anything that looks inside the application is a white box test. That's the wrong way to think about unit tests. For unit tests, the "box" is a class, not the application.
Wim Coenen