7 Common Unit Testing Mistakes (And How to Avoid Them)
Make your code bug-free and become a confident developer
👋 Hi, this is Dishit with this week’s newsletter. I write about software engineering, clean code and developer productivity.
Today, I will do something special.
If you subscribe to this newsletter, you will get a FREE code review session.
Thank you for your readership 🙏 🎉
Are you afraid to make any changes to the code because it might cause production incidents?
Are you suffering from burnout with constant firefighting with the production code?
Do you find that software test engineers find the most bugs in your code?
The problem could be that you are not writing unit tests right.
It is a common problem in the industry as writing unit tests are treated as a chore in most organisations.
Teams want fast delivery and writing unit tests slows down the delivery.
Tests are like the break in your car. It gives you the freedom to go fast.
The problem does not start there. The problem begins with the way programming is taught in most places.
Writing unit tests is one of the later chapters that you learn when learning programming.
As a developer, you do not value writing tests when you could already smash out code that works out of the box.
I understand that as I felt the same way. I also did not understand why you have to prove that your code works when you already know it works.`
So you tend to avoid it.
But once you start writing code in a job, you discover you need to write those unit tests along with the code.
But the problem is you have never learned it right.
You also do not have time to invest in learning to write tests.
So here are the 10 common mistakes most developers make:
1. Not Isolating Tests
Unit tests should test a single unit of code in isolation. Dependencies should be mocked or stubbed. Testing multiple units together turns it into an integration test.
Mistake
@Test
public void testServiceMethod() {
Service service = new Service();
Database database = new Database(); // Real database, not a mock
service.setDatabase(database); // Injecting a real dependency
assertEquals("ExpectedResult", service.methodUnderTest());
}
Fix
@Test
public void testServiceMethod() {
Service service = new Service();
Database mockDatabase = Mockito.mock(Database.class);
Mockito.when(mockDatabase.getData()).thenReturn("MockData");
service.setDatabase(mockDatabase); // Injecting a mock dependency
assertEquals("ExpectedResult", service.methodUnderTest());
}
This could also lead to flaky tests.
Mistake
@Test
public void testDatabaseQuery() {
Database database = new Database(); // Real database connection
database.connect();
List<User> users = database.query("SELECT * FROM users");
assertEquals(10, users.size());
}
The problem in the above code is that if the values in the database change then the test fails. e.g. If someone adds a row without your knowledge above test will return 11 and the assertion will fail.
Fix
@Test
public void testDatabaseQuery() {
Database mockDatabase = Mockito.mock(Database.class);
List<User> mockUsers = Arrays.asList(new User("user1"), new User("user2"));
Mockito.when(mockDatabase.query("SELECT * FROM users")).thenReturn(mockUsers);
List<User> users = mockDatabase.query("SELECT * FROM users");
assertEquals(2, users.size());
}
2. Ignoring Edge Cases
Ignoring edge cases can lead to missing bugs that occur under special conditions.
Mistake
@Test
public void testPositiveNumbers() {
assertEquals(2, MathUtil.add(1, 1));
}
Fix
@Test
public void testAdd() {
assertEquals(2, MathUtil.add(1, 1));
assertEquals(0, MathUtil.add(-1, 1));
assertEquals(-2, MathUtil.add(-1, -1));
assertEquals(Integer.MAX_VALUE, MathUtil.add(Integer.MAX_VALUE, 0));
}
3. Not Using Assertions Properly
Failing to use proper assertions can lead to tests that pass even when the code is incorrect.
Mistake
@Test
public void testStringConcat() {
String result = StringUtil.concat("Hello", "World");
if (!result.equals("HelloWorld")) {
throw new RuntimeException("Test failed");
}
}
Fix
@Test
public void testStringConcat() {
String result = StringUtil.concat("Hello", "World");
assertEquals("HelloWorld", result);
}
4. Testing Implementation Details Instead of Behavior
Tests should focus on the behavior of the code rather than its implementation details. This ensures that tests remain valid even if the internal implementation changes, as long as the behavior remains the same.
Mistake
@Test
public void testSortingImplementation() {
List<Integer> list = Arrays.asList(3, 2, 1);
list.sort(Integer::compareTo);
assertTrue(list.get(0) < list.get(1) && list.get(1) < list.get(2));
}
Fix
@Test
public void testSortingBehavior() {
List<Integer> list = Arrays.asList(3, 2, 1);
List<Integer> sortedList = SortUtil.sort(list);
assertEquals(Arrays.asList(1, 2, 3), sortedList);
}
5. Ignoring Maintainability
Tests should be easy to read, understand, and maintain. Complex and convoluted test cases make it difficult to diagnose issues.
Each test should have 1 assert.
Each test should test one concept or 1 behaviour. If it tests multiple concepts, the test will be complex and hard to maintain.
You want your tests to be dumb.
Mistake
@Test
public void testComplexScenario() {
Service service = new Service();
service.setConfig("config1", "value1");
service.setConfig("config2", "value2");
service.initialize();
assertTrue(service.isInitialized());
service.performAction("action1");
assertEquals("result1", service.getResult());
service.performAction("action2");
assertEquals("result2", service.getResult());
}
Fix
@Test
public void testInitialization() {
Service service = new Service();
service.setConfig("config1", "value1");
service.setConfig("config2", "value2");
service.initialize();
assertTrue(service.isInitialized());
}
@Test
public void testPerformAction1() {
Service service = new Service();
service.initialize();
service.performAction("action1");
assertEquals("result1", service.getResult());
}
@Test
public void testPerformAction2() {
Service service = new Service();
service.initialize();
service.performAction("action2");
assertEquals("result2", service.getResult());
}
6. Not Testing for Exceptions
Failing to test for expected exceptions can result in unhandled edge cases and bugs.
Test exception is hard. So simplify by minimising exceptions. If possible, only let the top-level service handle the exception. This depends on the context.
But not testing an exception can cause real pain so beware.
Mistake
@Test
public void testInvalidInput() {
Service service = new Service();
service.processInput("validInput");
// No test for invalid input
}
Fix
@Test(expected = IllegalArgumentException.class)
public void testInvalidInput() {
Service service = new Service();
service.processInput("invalidInput");
}
7. Not Cleaning Up After Tests
Tests should clean up any resources they use to avoid side effects on other tests.
If cleaning up is not done, it will make your tests flaky.
The problem with flaky tests is they make your tests unreliable.
It is as good as if there are no tests.
Mistake
@Test
public void testFileCreation() throws IOException {
File file = new File("testFile.txt");
FileWriter writer = new FileWriter(file);
writer.write("Test data");
writer.close();
assertTrue(file.exists());
}
Fix
@Test
public void testFileCreation() throws IOException {
File file = new File("testFile.txt");
try (FileWriter writer = new FileWriter(file)) {
writer.write("Test data");
}
assertTrue(file.exists());
if (file.exists()) {
file.delete();
}
}
Now these are some of the common problem that can be solved.
But this list is not complete as there might be some scenarios that will be unique to your situation.
So I am giving Free code review sessions if you subscribe to the newsletter.
Existing Subscribers
Being an existing subscriber, you also get a FREE code review session. Reply to this email with “REVIEW” and I will send your the booking details.
Meanwhile, please recommend this newsletter if you liked the article.