Integration vs. unit tests
You should keep your unit tests and your integration tests completely separated. Your unit tests should test one thing and one thing only and in complete isolation of the rest of your system. A unit is loosely defined but it usually boils down to a method or a function.
It makes sense to have tests for each unit so you know their algorithms are implemented correctly and you immediately know what went wrong where, if the implementation is flawed.
Since you test in complete isolation while unit-testing you use stub and mock objects to behave like the rest of your application. This is where integration tests come in. Testing all units in isolation is great but you do need to know if the units are actually working together.
This means knowing if a model is actually stored in a database or if a warning is really issued after algorithm X fails.
Test driven development
Taking it a step back and looking at Test Driven Development (TDD) there are several things to take into account.
- You write your unit test before you actually write the code that makes it pass.
- You make the test pass, write just enough code to accomplish this.
- Now that the test passes it is time to take a step back. Is there anything to refactor with this new functionality in place? You can do this safely since everything is covered by tests.
Integration first vs Integration last
Integration tests fit into this TDD cycle in one of two ways. I know of people who like to write them beforehand. They call an integration test an end-to-end test and define an end to end test as a test that completely tests the whole path of a usecase (think of setting up an application, bootstrapping it, going to a controller, executing it, checking for the result, output, etc...). Then they start out with their first unit test, make it pass, add a second, make it pass, etc... Slowly more and more parts of the integration test pass as well until the feature is finished.
The other style is building a feature unit test by unit test and adding the integration tests deemed necessary afterwards. The big difference between these two is that in the case of integration test first you're forced to think of the design of an application. This kind of disagrees with the premise that TDD is about application design as much as about testing.
Practicalities
At my job we have all our tests in the same project. There are different groups however. The continuous integration tool runs what are marked as unit tests first. Only if those succeed are the slower (because they make real requests, use real databases, etc) integration tests executed as well.
We usually use one test file for one class by the way.
Suggested reading
- Growing object-oriented software, guided by tests This book is an extremely good example of the integration test first methodology
- The art of unit testing, with examples in dot.net On unit testing, with examples in dot.net :D Very good book on principles behind unit-testing.
- Robert C. Martin on TDD (Free articles): Do read the first two articles he linked there as well.
Perspective:
So let's take a step back and ask what TDD is trying to help us with. TDD is trying to help us determine if our code is correct or not. And by correct, I mean "does the code meet the business requirements?" The selling point is that we know changes will be required in the future, and we want to make sure that our code remains correct after we make those changes.
I bring that perspective up because I think it's easy to get lost in the details and lose sight of what we're trying to achieve.
Principles - SAP:
While I'm not an expert in TDD, I think you're missing part of what Single Assertion Principle (SAP) is trying to teach. SAP can be restated as "test one thing at a time." But TOTAT doesn't roll off the tongue as easily as SAP does.
Testing one thing at a time means that you focus on one case; one path; one boundary condition; one error case; one whatever per test. And the driving idea behind that is you need to know what broke when the test case fails, so you can resolve the issue more quickly. If you test multiple conditions (ie. more than one thing) within a test and the test fails, then you have a lot more work on your hands. You first have to identify which of the multiple cases failed and then figure out why that case failed.
If you test one thing at a time, your search scope is a lot smaller and the defect is identified more quickly. Keep in mind that "test one thing at a time" doesn't necessarily exclude you from looking at more than one process output at a time. For example, when testing a "known good path", I may expect to see a specific, resulting value in foo
as well as another value in bar
and I may verify that foo != bar
as part of my test. The key is to logically group the output checks based upon the case being tested.
Principles - PMP:
Likewise, I think you're missing a bit about what Private Method Principle (PMP) has to teach us. PMP encourages us to treat the system like a black box. For a given input, you should get a given output. You don't care how the black box generates the output. You only care that your outputs align with your inputs.
PMP is really good perspective for looking at the API aspects of your code. It can also help you scope what you have to test. Identify your interface points and verify that they meet the terms of their contracts. You don't need to care about how the behind-the-interface (aka private) methods do their job. You just need to verify they did what they were supposed to do.
Applied TDD (for you)
So your situation presents a bit of a wrinkle beyond an ordinary application. Your app's methods are stateful, so their output is contingent upon not only the input but also what's been previously done. I'm sure I should <insert some lecture>
here about state being horrible and blah blah blah, but that really doesn't help solve your problem.
I'm going to assume you have some sort of state diagram table that shows the various potential states and what needs to be done in order to trigger a transition. If you don't, you're going to need it as it will help express the business requirements for this system.
The Tests: First, you're going to end up with a set of tests that enact state change. Ideally, you'll have tests that exercise the full range of state changes that can occur but I can see a few scenarios where you may not need to go that full extent.
Next, you need to build tests to validate the data processing. Some of those state tests will be reused when you create the data processing tests. For example, suppose you have a method Foo()
that has different outputs based upon an Init
and State1
states. You'll want to use your ChangeFooToState1
test as a setup step in order to test the output when "Foo()
is in State1
".
There's some implications behind that approach that I want to mention. Spoiler, this is where I'll infuriate the purists
First off, you have to accept that you using something as a test in one situation and a setup in another situation. On the one hand, this seems to be a direct violation of SAP. But if you logically frame ChangeFooToState1
as having two purposes then you're still meeting the spirit of what SAP is teaching us. When you need to make sure Foo()
changes states, then you use ChangeFooToState1
as a test. And when need to validate "Foo()
's output when in State1
" then you're using ChangeFooToState1
as a setup.
The second item is that from a practical point of view, you're not going to want fully randomized unit testing for your system. You ought to run all of the state change tests prior to running the output validation tests. SAP is kind of the guiding principle behind that ordering. To state what should be obvious - you can't use something as setup if it fails as a test.
Putting it together:
Using your state diagram, you'll generate tests to cover the transitions. Again, using your diagram, you generate tests to cover all of the input / output data processing cases driven by state.
If you follow that approach, the bloated, complicated, long, and difficult to write
tests should get a little bit easier to manage. In general, they should end up smaller and they should be more concise (ie less complicated). You should notice that the tests are more decoupled or modular as well.
Now, I'm not saying the process will be completely pain free because writing good tests does take some effort. And some of them will still be difficult because you're mapping a second parameter (state) over quite a few of your cases. And as an aside, it should be a little more apparent why a stateless system is a easier to build tests for. But if you adapt this approach for your application, you should find that you are able to prove your application is working correctly.
Best Answer
A possible solution would be to move the testing portion from the development machines to a continuous integration setup (Jenkins for example) using version control software of some flavor (git, svn, etc...).
When new code has to be written the given developer will create a branch for whatever they are doing in the repository. All work will be done in this branch and they can commit their changes to the branch at any time without messing up the main line of code.
When the given feature, bug fix, or whatever else they are working on has been completed that branch can be merged back into the trunk (or however you prefer to do it) where all unit tests are run. If a test fails the merge is rejected and the developer is notified so they can fix the errors.
You can also have your CI server run the unit tests on each feature branch as commits are made. This way the developer can make some changes, commit the code, and let the server run the tests in the background while they continue to work on additional changes or other projects.
A great guide to one way of doing such a setup can be found here (git specific but should work for other version control systems): http://nvie.com/posts/a-successful-git-branching-model/