Unit Testing in a Nutshell

Unit Testing in a Nutshell
Dominik Klotz
March 29, 2023
Share
linkedin iconmail icon

In this blog post we are going to cover everything you need to know about Unit Tests. This is the first of two blog posts covering Unit Testing. By the end of this post you will have an overview of all the important characteristics, techniques and patterns.

Let’s start with a quote by Martin Fowler:

“Every fool can write code that a computer can understand. Good programmers write code that humans understand.”

Analogous, good Unit Tests are comprehensible to other people, not just machines. Making code and in this case Unit Tests comprehensible to everyone is what we want to call the Art of Unit Testing.

Why do you need to test?

  1. Write working code
  2. Keep code working
  3. Develop faster
  4. Find bugs before production
  5. Force us to write testable code
  6. Humans do mistakes

There are six reasons, the most obvious answer (1) is that you want your code to work properly. But you need to make sure that it (2) keeps working, even when you change the code. Even the tiniest changes can lead to a series of unintended errors. You don’t want your users to find bugs, you want to (3) find bugs before production. The release cycles of updates heavily increased over the last years which means that the (4) development-process needs to be done more quickly. A well written test is the foundation to this. (5) Writing testable code from the beginning is an investment that pays off in the long run, the amount of work just piles up over time. Last but not least, (6) humans make mistakes. Making mistakes is not a problem, not finding them is a problem.

How Are Unit Tests defined?

There are various different definitions of Unit Tests, so let’s stick to Martin Fowler one more time, who identified three core characteristics of Unit Tests that every definition shares: Firstly there is a notion that unit tests are low-level, focusing on a small part of the software system. Secondly unit tests are usually written by the programmers themselves using their language dependent unit testing framework, like JUnit, MSTest or NUnit. Thirdly unit tests are expected to be significantly faster than other kinds of tests.

Elliotte Rusty Harold stripped the definition of Unit Tests down to: “verifying that a known fixed input produces a known fixed output”.
But first step back. What is a unit in general? A unit is normally a method, constructor or deconstructor.

Call dependencies between units

Units that are to be tested often rely on other units to fulfill their behavior. When testing a unit, all dependencies should be replaced by mocks. A mock object is a simulated object which simulates the behavior of the dependencies. You only want to test the functionality of this unit and not the functionality of the other ones.

The test calls the unit and it will call our mocks. At the end, we verify that the mocks are called with the expected parameters. (See image below)

Mock call dependencies. Then verify calls with expected parameters

What Is the FIRST-Principle?

When it comes to the properties of Unit Testing, Brett Schuchert and Tim Ottinger came up with the FIRST Principle: Fast, Independent, Repeatable, Self-Checking and Timely. Writing your Unit Tests with these properties in mind keeps your code clean.

Your Unit Test has to be fast. Most projects cover hundreds of thousands of tests. A test that takes 0.5 or even 0.25 seconds to complete is unacceptably slow. Time is money, make your tests as fast as possible.

An independent test does not rely on any subset in a specific order. You can run every test without constraint. Passing or failing should not depend on the order in which the test was run. Tim Ottinger also refers to the “I” in FIRST as isolated.

Repeatable means that tests must be able to be run repeatedly without intervention. They must not depend upon a specific initial state and they must not leave any problems behind that would prevent them from being re-run. They have to be run repeatedly in any order at any time.

Self-checking or self-validating means that a test can automatically detect if it was passed or failed. There is no additional instance needed to confirm or validate the result.

Tests are written at the right time, immediately before the code that makes the tests pass. Writing the test first makes a difference, don’t write them after you write your code.

Do You Know Your Test Coverage?

Let’s talk about Code Coverage or Test Coverage as some like to call it. Coverage shows you which lines and branches of the code were (or were not) covered by the tests. It is also a metric which helps you to find out the percentage of your covered (executed) code by the tests. E.g.: It tells you that your codebase consists of 10 lines, 8 lines were being covered by your tests, so your coverage is 80%. While this gives you no information about the quality of your software or how good your tests really are. The coverage of the complete code base should be higher than 80%.

Code coverage is useful for core refactoring, too. Execute it before you start, check which code branches are covered, add the missing tests and start refactoring your tests.

What Is the Best Way to Write Unit Tests?

Last but not least: how should your Unit Tests be written? What are the Do’s and Don’ts, what do the naming conventions and building patterns look like and how is the code structured? Let’s start by prefacing some general tips before you actually start writing your code.
In the FIRST-Principle we already came across the term clean code. The term deviates from the Clean Code book by Robert Cecil Martin. In a nutshell, clean code is any code that can be understood intuitively – by others, not just by yourself. Next up is Dependency Injection (DI) – it’s not necessary but highly recommended to isolate your dependencies in the code. This allows you to easily inject your mocks to your class and only test the unit.

Some general Do’s you should consider:

  • One assert per test method
  • Share setup and fixture
  • Multiple test classes per class

Avoid these Don’ts:

  • Generate random input
  • Access network or file system
  • Names constants from model code.
  • Conditions
  • Using description annotations

First code example: to avoid god testing classes you can split your test class in multiple test classes. Then test only a single method in your test class, as shown in the next code example.

-- CODE language-ts line-numbers -- class CreateUserServiceTest { UserService user; @Test void shouldCreateUser(){ ... } @Test void shouldThrowExcpetionWhenUserAlreadyExists(){ ... } } class UpdateUserServiceTest { UserService user; @Test void shouldUpdateUser(){ ... } @Test void shouldThrowExcpetionWhenUserIsNotFound(){ ... } }

Second code example: Never ever (!) use conditions or loops in test methods. Each test method should only test one test case. Split them in multiple test methods.

-- CODE language-ts line-numbers -- @Test void donts(){ if(condition){ assertTrue(user.isAdult()); } else{ assertFalse(user.isAdult()); } }

Third code example: Warning! When you call static methods from external dependencies, the behavior is not only changed in this test method it is also changed in all other test methods. If you run them at the same time it can result in flaky tests.

-- CODE language-ts line-numbers -- @Test void shouldThrowExceptionWhenConnectionIsTimedOut(){ ExternalDependency.setTimeout(10); ... assertThrows(...); }

Have You Already Thought About Naming Your Test Methods?

Talking about intuitive, clean and easy to understand code (for everybody), let’s have a look at a few common naming conventions for test methods. There are some general recommendations regarding test naming (Stefanovskiy):

  • Test name should express a specific requirement
  • Test name could include the expected input or state and the expected result for that input or state
  • Test name should be presented as a statement or fact of life that expresses workflows and outputs
  • Test name could include the name of the tested method or class

An example might be:

  1. <MethodName>_<StateUnderTest>_<ExpectedBehavior>
    isEighteen_AgeGreaterThan18_True
    Cons:
    Renaming of method name is necessary, when renaming the origin method
  2. <MethodName>_<ExpectedBehavior>_<StateUnderTests>
    isEighteen_True_AgeGreaterThan18
    Cons:
    Renaming of method name is necessary, when renaming the origin method
  3. test[Feature being tested]
    testIsEighteenIfAgeGreaterThan18
    Cons:
    test-prefix is duplicated information in combination of an @Test-annotation
  4. Feature to be tested
    isEighteenIfAgeGreaterThan18
    Cons:
    Expected result is not defined
  5. Should_<ExpectedBehavior>_When_<StateUnderTest>
    Should_True_When_AgeGreaterThan18
    Cons:
    Long name through should and when
  6. When_<StadeUnderTest>_Expect_<ExpectedBehavior>
    When_AgeGreaterThan18_True
    Cons:
    Long name through should and expect
  7. Given_<Prediction>_When_<StateUnderTest>_Then_<ExpectedBehavior>
    Give_UserIsUnder18_When_AgeGreaterThan18_Then_False
    Cons:
    Given, When and then are duplicated

It does not matter which naming convention you choose, but your team should use one consistently. Personally I prefer a combination of 1 and 5 <MethodName>_Should_<ExpectedBehavior>_When_<StateUnderTest>.

Do You Know These Test Doubles?

When writing your code, knowing the following terms is crucial:

Dummy

The most basic term you have to know is the Dummy. A Dummy is a placeholder required to pass the unit. The Dummy should not be accessed during the test. The simplest example might be null:

-- CODE language-ts line-numbers -- DummyDependency dummy = null; //Dummy Foo foo = new Foo(dummy); foo.call(dummy)

Fake

In automated testing it is common to use objects that implement the production interface and behave like it. Fakes are objects that have working implementations, but not the same as the production one. Fakes simplify the complexity of the external dependency setup. Fakes are frequently used for database access, openid connect endpoints or external APIs.

-- CODE language-ts line-numbers -- class FakeUserRepository implements UserRepository{ List users = new ArrayList(); void addUser(User user){ users.add(user); } void getUsers(){ return users; } }

Stub

Stub is an object that holds predefined data and uses it to answer calls during tests. It provides indirect input to the unit from a dependency. Classic examples are objects, exceptions or primitive values which are returned from a method call.

-- CODE language-ts line-numbers -- class StubUserRepo extends UserRepo { String return Value; public StubUserRepo(String returnValue){ this.returnValue = returnValue; } public String getNameFor(String i){ return returnValue; } } ... // Test Methode StubUserRepo stubUserRepo = new StubUserRepo("Carl"); UserService userService = new UserService(stubUserRepo); String acturalName = userService.getNameFor(100); assertEquals("Carl", acturalName);

Mock

Compared to a stub, allow mocks to verifying method invocation of the unit. An example is the service method invoking the repository method.

-- CODE language-ts line-numbers -- class MockUserRep extends UserRepo { boolean addUserWasCalled = false; public String addUser(String i){ addUserWasCalled = true; } public boolean verify(){ return addUserWasCalled; } } ... // Test Methode MockUserRepo mockUserRepo = new MockUserRep(); UserService userService = new UserService(mockUserRepo); userService.addUser("Peter"); assertTrue(mockUserRepo.verify());

Spy

A Spy uses the original implementation and is the most complex version of a test double. It is a wrapper of the original implementation, so it records the number of calls and not the input. For example:

-- CODE language-ts line-numbers -- class SpyUserRepo extends UserRepo { int count = 0; String userRepo; public StubUserRepo( UserRep userRepo){ this.userRepo = userRepo; } public void addUser(String name){ userRepo.addUser(name); count += 1; } // Getters } ... // Test Methode SpyUserRepo spyUserRepo = new SpyUserRepo(new UserRep()); UserService userService = new UserService(spyUserRepo); String acturalName = userService.addTwoUsers("Peter", "Gorg"); assertEquals(2, spyUserRepo.getCount());

How Are Test Methods Structured?

There are two common practices to structure your test method. First the AAA-Pattern and the BDD-Pattern. The BDD-Pattern is not covered here.

The AAA-Pattern is commonly used in unit tests. This Acronym stands for arrange, act and assert (Bill Wake). The test method code is structured in three blocks, as shown in the following example.

-- CODE language-ts line-numbers -- @Test void sqrt_should_2_when_4{ // Arrange int input = 4; // Act int output = Math.sqrt(input); // Assert assertEquals(2, output) }

Now let’s come to a conclusion. Unit Testing is an art that is easy to learn (and not too hard to master) if you follow the instructions given in this overview. Now that you know about all the characteristics, basic coding examples and patterns, we suggest you to always question your code (as Fowler would say): is my code comprehensible to everyone?

This blog is based on a Unit Test input Dominik Klotz gave for the Karlsruher Testing Community.

Get in touch

For media queries, drop us a message at info@askui.com