| >I'm not sure what you mean by "Realistic" tests. * Where possible, use the real thing - that is, use an actual database in preference to, e.g. mock objects representing an object that you use to interact with a database. * Where you need to use a mock (when the real thing is too expensive to use), make it a realistic mock. >Perhaps you mean integrated tests that exercise, say, the entire stack from handling the HTTP request down to the database, if we're talking about a REST API. Exactly, but if, say, there was some algorithmic code in there that could be exercised realistically without using the HTTP stack then the HTTP stack is not necessarily necessary. >If you don't care much about covering large portions of the state space Code coverage is entirely orthogonal to the type of test you are doing. >your tolerance for defects is high, then I'm sure your claim about improved productivity is true. You appear to be fairly confused about the distinction between test coverage and modes of testing. >My tolerance for defects is low If you put no emphasis on test realism then I suspect that your tolerance will naturally have to be quite high. >I've tried doing this with integrated tests, and found it to be very difficult to do in a timely manner. I found this too sometimes. It's a tooling issue. Integration test tooling is more expensive to build initially but it tends to be much more reusable than unit test tooling. SMTP client stubs may be quicker to build but relatively useless in future projects that use different libraries (sometimes even different versions of the same library) whereas a mock SMTP server is useful forever. >In an integrated test, I have to set up too much state and have the machine do too much irrelevant work just to verify this small portion of the code. I have no problem with my tests chewing up 10,000% more CPU than yours and taking an extra 5 minutes if they've got even a 2% higher chance of finding bugs. CPU time is cheap, my time is expensive, bugs are expensive. Easy trade off. >That leaves coupling and maintenance. This is a valid concern in the mockist style, but I haven't found it to be a limiting factor in maintaining my code. Still means you're writing and maintaining more code. That means more bugs and more work. > think it comes down to testing the right things. If you're using mocks to verify a contract between two objects, and you want to change that contract, naturally your tests for the contract will break. Yup, and since changing the contracts between different subsystems is the most critical and important part of refactoring code, you've just tossed out a large chunk of the mainr benefit of having tests - safe refactoring. This is the worst aspect of unit testing IMO - it cements technical debt because refactoring changes contracts that will turn the tests red. |
I did not say they were equivalent. Rather, I instead made a connection to the effort required to cover the state space in a given mode of testing. I've found it takes more integration tests to cover the state space than it does with unit tests, given the multiplicative nature of code paths in each layer. J.B. explains this in his talk, and I buy his argument, since I've experienced it myself.
For example, you could collapse several different types of failure at a lower layer into one code path in a higher layer using an exception, and verify that the HTTP layer returns the same thing when that exception happens. In this situation, you avoid having to set up the state necessary to cause the exception that the errors have been collapsed to, which may be 3 or 4 layers deeper. This means less test code, and less effort on my part.
> You appear to be fairly confused about the distinction between test coverage and modes of testing.
Nope, as explained above, the effort required to cover the state space is directly influenced by the mode of testing. For example, if you tested via typing on the keyboard with a pencil, you'd have to expend more effort to cover your state space. Of course, integration isn't as bad as pencil-testing, but the point is that one is easier than the other with respect to state space coverage.
Regarding reusable tooling - I've found this true as well. But tooling doesn't solve the whole problem. My main point is the amount of set up necessary to trigger a certain behavior, which tends to be too time consuming for me to justify the time. You can use tooling here as well, perhaps with data builders and object mothers. But that is also too much setup effort for my tastes.
> I have no problem with my tests chewing up 10,000% more CPU than yours and taking an extra 5 minutes if they've got even a 2% higher chance of finding bugs. CPU time is cheap, my time is expensive, bugs are expensive. Easy trade off.
Here you seem to assert that integration tests are more likely to find bugs. This is highly dependent on your skill in designing testable code that can be exercised without irrelevant portions of the stack. However, you do have a point that integration tests can find classes of bugs that unit tests may not (environmental setup, connection issues, etc.). I don't unit test those things of course.
Regarding CPU and the 5 minute difference - try 25 minutes difference. I have seen this in practice, and at that scale it really slows down the team. You can ease the pain with more machines, but I'd rather have focused, fast tests that can run right on my machine in as short a time as possible. I value the fast feedback loop.
> Still means you're writing and maintaining more code. That means more bugs and more work.
I haven't found this to be a problem. Sometimes I view messages between objects as behavior, so if the tests are inspecting that then I would actually want them to break if I change messages between objects. I've often found that if this is a hindrance to refactoring, there are problems in the code itself, not just the tests.
>Yup, and since changing the contracts between different subsystems is the most critical and important part of refactoring code...
I don't think that's true. If I'm changing contracts so often that it's a burden to maintain, I've likely missed an abstraction opportunity. Instead of blaming unit tests, I just fix the problem. Further, refactoring is not equivalent to changing subsystems. It encompasses many more useful activities that improve your code for the better. And further, if you take the stance that object interactions are behavior (sometimes, when useful), then changing that is not a refactor, but a re_work_. My favorite article on refactor vs. rework: http://www.daedtech.com/rewrite-or-refactor/
Thanks for the spirited debate!