The Worst Thing to Do When Writing Tests
Logic in Tests
When you are writing any type of test, the number one thing that should be avoided is logic in tests. Tests are meant to verify your production code is executing as expected. If you add logic into tests, how do you know you won't have a bug in the test? You might be getting a false positive because the test passes when it shouldn't. It is possible there wasn't an issue when it was written, but as the code grows, will the test still not have any bugs?
Sometimes logic is put in tests because of code that isn't written well. It can be either in production or test code. If it is a test testing too many things, then the fix is to break it into multiple tests.
Know Your Testing Framework
In this article, JUnit will be the testing framework that is used for examples.
One reason for putting logic in tests is so you can fail the test.
@Test
public void returnsFalseWhenNumberIsNegative() {
if (isPositive(-1)) {
fail("Number is not positive");
}
}
Testing frameworks have the methods built in to avoid this type of logic. The code example above can be rewritten using JUnit's assertions.
@Test
public void returnsFalseWhenNumberIsNegative() {
assertFalse(isPositive(-1));
}
Another reason for logic in tests is that you may need to run one or more tests against one environment, but they don't apply to another. You can do this with JUnit's assumeThat method.
@Test
public void testSomethingThatIsInDemoModeOnly() {
assumeThat(
"Required for demo mode only.",
environment.isDemoMode(),
is(true)
);
// test code...
}
This will skip the test if it doesn't apply to the condition in the assumeThat method.
If you are utilizing your test framework as it was intended to be used, this logic is simple to avoid.
Know the Data You Are Working With
The biggest reason you will see logic in test code is when you don't know what state the data is in that you are working with.
@Test
public void test() {
if (file.exists() && file.isDirectory()) {
}
}
This if statement exists because we don't know if the file has been setup or if it has been setup correctly. The simple fix for this is to setup the data in the test. That way you know what data you are working with.
Sometimes code is written like production code. In the example above, it is checking to see if a file exists and that it is a directory. This is something you should only do in production. If this test fails because you are trying to do something on a file that doesn't exist or isn't a directory, it should just fail. This tells you that the test data wasn't setup correctly. Test code shouldn't have those types of checks in them and should be allowed to fail.
Another common reason you may not know what data you are working with is that you are using class level variables when you shouldn't be. This can cause logic in tests and logic in methods that run before and after a test runs. This is shown in the example below in a method that runs after each test.
public final class MyTest {
private File file;
@AfterEach
public void cleanUp() {
if (file != null) {
file.delete();
}
}
}
What Do You Do When You Can't Avoid Logic?
There are going to be times where you have to put logic into tests. This is rare if you are using good coding practices. If there is logic that can be extracted and reused, put it in an external library that other projects can use too. This allows you to write tests for the logic in these methods. If this can't be done, then always document and explain why you had to put logic in the test.
Conclusion
Always avoid logic in tests. Tests should only have one pathway of execution, so they execute the same way every time they are run.