10 best practices when writing a Unit Test?

Valéry Raulet
All About Software Testing
4 min readJan 27, 2023

--

Photo by Kaitlyn Baker on Unsplash

So you decided to write Unit Tests, that’s good! But, like anything in life, you need to put effort to write useful Unit Tests. In this short article, I want to give a few pieces of advice on what to look for to make your Unit Tests better than ever!

Without further ado, find the 10 best practices to follow:

1. Small & Focused

A Unit Test tests a unit, hence its name. So, it should test a single, specific aspect of the code.

Keep it as small as possible, i.e. you don’t want to start having bugs in your tests as well…

And focused on testing 1 aspect of your code. Avoid testing multiple behaviours in the same test.

2. Test for the expected outcome

A Unit Test should test, so that means you need to check the code behaves as expected. You need to make sure you get the correct output and error conditions.

3. Test Doubles

Again, you want your test to focus on 1 specific aspect of your code. That means, you need to remove anything around that is not related to your test.

Test doubles like mocks, stubs and fakes are useful to isolate the unit being tested and avoid test dependencies.

4. Repeatable & Independent

A test should not depend on another test and repeating the same test over and over should always return the same outcome.

Unit tests should be repeatable and independent, meaning that running one test should not affect the outcome of any other test.

5. Use Assert statements

You should follow a pattern like AAA (Arrange, Act and Assert). Your test should have 3 sections:

  • Arrange: setup preconditions and inputs;
  • Act: perform the action on the code you want to test;
  • Assert: validate that the outcome is as expected.

Example:

[Test]
public void TestCorrectCount()
{
// Arrange
var users = new UserDto[]
{
new UserDto { Age = 5, Firstname = "John", Lastname = "Doe" },
new UserDto { Age = 25, Firstname = "Marcel", Lastname = "Proust" },
new UserDto { Age = 17, Firstname = "Elon", Lastname = "Axe" },
new UserDto { Age = 18, Firstname = "Gated", Lastname = "Musk" },
new UserDto { Age = 0, Firstname = "Invoice", Lastname = "Fences" }
};

// Act
var userManagement = new UserManagement(users);

// Assert
Assert.That(userManagement.AdultCount, Is.EqualTo(2));
Assert.That(userManagement.MinorCount, Is.EqualTo(3));
}

Use assert statements to check the actual output of the code against the expected output. This makes it easy to pinpoint where the code is wrong: when a test fails!

6. Use meaningful names

Use meaningful and descriptive names for test methods and test classes. This makes it easy to understand what the test is for and how it is organized.

// Give a (long) name that makes it obvious what is being tested.
public void UserManagementShouldThrowAnExceptionWhenAgeIsNegative() {}

7. Test Edge Cases

Boundary analysis is a testing method where we identify values that modify the behaviour of the code. You want to make sure you test those edge cases to ensure the code reacts appropriately.

You also need to test “invalid” or unusual values to ensure the code responds correctly.

In the example below, we test for the age of 22, 23, 55 and 56 which are the boundaries from the switch statement:

public class LoanUserDto
{
public int NumberOfLoans { get; set; }
public bool AlreadyHadBankrupcies { get; set; }
public int Age { get; set; }
}

public class Loan
{
public static double Score(LoanUserDto user)
{
var score = user.Age switch
{
<= 22 => 0.0,
> 22 and < 56 => 1 - user.NumberOfLoans * 0.25 * (55 / user.Age),
_ => 1 - user.NumberOfLoans * 0.48,
};

score = user.AlreadyHadBankrupcies ? score / 2.0 : score;

return score;
}
}

public class LoanTests
{
[Test]
[TestCase(0, false, 22, 0.0)]
[TestCase(0, false, 23, 1.0)]
[TestCase(0, false, 55, 1.0)]
[TestCase(0, false, 56, 1.0)]
public void TestZeroLoanNoBankrupcy(int numberOfLoans, bool alreadyHadBankrupcies, int age, double expectedScore)
{
var user = new LoanUserDto { NumberOfLoans = numberOfLoans, AlreadyHadBankrupcies = alreadyHadBankrupcies, Age = age };

var score = Loan.Score(user);

Assert.That(score, Is.EqualTo(expectedScore));
}

[Test]
[TestCase(1, false, 22, 0.0)]
[TestCase(1, false, 23, 0.5)]
[TestCase(1, false, 55, 0.75)]
[TestCase(1, false, 56, 0.52)]
public void TestOneLoanNoBankrupcy(int numberOfLoans, bool alreadyHadBankrupcies, int age, double expectedScore)
{
var user = new LoanUserDto { NumberOfLoans = numberOfLoans, AlreadyHadBankrupcies = alreadyHadBankrupcies, Age = age };

var score = Loan.Score(user);

Assert.That(score, Is.EqualTo(expectedScore));
}
}

8. KISS: Keep It Simple

Keep your tests as simple as possible. If you add complex logic or advanced features to your tests, you are more likely to introduce errors in your tests and end up not testing appropriately.

9. Document the test

It is not always obvious why a test has been written. By using a meaningful name (see 6. Use meaningful names), you should already understand the purpose of a test but sometimes, that is not enough!

Do not hesitate to document your test to explain what problem the test is solving.

10. Keep your tests up-to-date

If the code changes, update the tests accordingly. For instance, if you add a new branch, add additional tests to exercise this new branch and ensure the outcome is still matching the requirements.

Do not forget, your tests will be valuable only if they are really testing something. It is easy to write a test, but not so much to write a useful test!

--

--

Valéry Raulet
All About Software Testing

I have been interested in business and technology since I was about 10. My interest spans across so many fields but I hope you’ll find my writing useful!