Unit Testing 101: From Zero to Hero

Do you want to start writing unit tests? But, you don’t know where to start? Do you want to adopt unit testing in your team? I can help you!

If you’re a beginner or a seasoned developer new to unit testing, this is the place for you. No prerequisites are needed.

Write it

Learn to write your first unit tests with MSTest. Read what a unit test is, why we need unit tests, and what makes a good unit test.

Identify and fix these four common mistakes when writing your first unit tests. Learn one of these four naming conventions and stick to it. Don’t worry about long test names.

Find these first three posts plus a summary of "The Art of Unit Testing" and my best tips from this series on my free ebook “Unit Testing 101”. Download your free copy on my Gumroad page or click on the image below.
Grab your own copy of Unit Testing 101

When writing your unit tests, make sure you don’t duplicate logic in Asserts. That’s THE most common mistake in unit testing. Tests should only contain assignments and method calls.

Improve it

Learn how to write good unit tests, avoiding complex setup scenarios and hidden test values.

Make sure to always write a failing test first. And make it fail for the right reasons.

Write tests easy to follow using simple test values.

Use Builders to create test data. And, learn how to write tests that use DateTime.Now.

Strive for a set of always-passing tests, a “Safe Green Zone.” For example, use a culture when parsing numeric strings, instead of relying on a default culture on developers’ machines.

Fake it

Learn what fakes are in unit testing and the difference between stubs and mocks. Follow these tips for better stubs and mocks in C#.

Read how to create fakes with Moq, an easy to use mocking library.

If you find yourself using lots of fakes, take advantage of automocking with TypeBuilder and AutoFixture to write simpler tests.

Master it

Read all the tips from this series on Unit Testing Best Practices.

Deep into assertions, check how to write better assertions and how to write custom assertions.

To see how to put these best practices in place, see how I refactored these real-world tests:

If you work with ASP.NET Core, learn how to write tests for HttpClient, Authorization filters, and logging and logging messages.

If you want to practice writing some unit tests, check my Unit Testing 101 repository on GitHub.

canro91/Testing101 - GitHub

Ready to upgrade your unit testing skills? Write readable and maintainable unit test with my course Mastering C# Unit Testing with Real-world Examples on Udemy. Learn unit testing best practices while refactoring real unit tests from my past projects. No tests for a Calculator class.

Happy testing!

Write custom Assertions to improve your tests

Last time, we went through some best practices to write better assertions on our tests. This time, let’s focus on how to use custom assertions to improve the readability of our tests.

Use custom assertions to encapsulate multiple assertions on a single method and express them in the same language as the domain model. Write custom assertions with local methods or extension methods on the result object of the tested method or on the fake objects.

We can either create custom assertions on top of the MSTest Assert class. And, our own Verify methods on Moq mocks.

1. How to write custom MSTest Assert methods

Let’s refactor one of our tests for Stringie, a (fictional) library to manipulate strings. We used Stringie to learn 4 common mistakes when writing tests and 4 test naming conventions.

To write custom assertions with MSTest, write an extension method on top of the Assert class. Then, compare the expected and actual parameters and throw an AssertFailedException if the comparison fails.

Let’s create a StringIsEmpty() method,

public static class CustomAssert
{
    public static void StringIsEmpty(this Assert assert, string actual)
    {
        if (string.IsNullOrEmpty(actual))
        {
            return;
        }

        throw new AssertFailedException($"Expect empty string but was {actual}");
    }
}

Then, we can use StringIsEmpty() with the That property. Like this,

Assert.That.StringIsEmpty("");

With this custom assertion in place, we can rewrite the Assert part of our tests for the Stringie Remove method. Like this,

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Stringie.UnitTests;

[TestClass]
public class RemoveTests
{
    [TestMethod]
    public void Remove_NoParameters_ReturnsEmpty()
    {
        string str = "Hello, world!";

        string transformed = str.Remove();

        Assert.That.StringIsEmpty(transformed);
        //          ^^^^^
    }
}

With custom assertions, like StringIsEmpty(), we can write our assertions using the same vocabulary from our business domain.

Shaving wood
Photo by Mike Kenneally on Unsplash

2. How to write custom Moq Verify method

If we’re using Moq, we can create our custom Verify() methods too.

Let’s write some tests for an API client. This time, we have a payment processing system and we want to provide our users a user-friendly client to call our endpoints.

We want to test that our methods call the right API endpoints. If we change the client version number, we should include the version number in the endpoint URL.

We could write some unit tests like these ones,

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Threading.Tasks;

namespace CustomAssertions;

[TestClass]
public class PaymentProxyTests
{
    [TestMethod]
    public async Task PayAsync_ByDefault_CallsLatestVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object);

        await proxy.PayAsync(AnyPaymentRequest);

        // Here we verify we called the right url
        fakeClient.Verify(x => x.PostAsync<PaymentRequest, ApiResult>(
            //     ^^^^^
            It.Is<Uri>(t => t.AbsoluteUri.Contains("/v2/pay", StringComparison.InvariantCultureIgnoreCase)), It.IsAny<PaymentRequest>()),
            Times.Once);
    }

    [TestMethod]
    public async Task PayAsync_VersionNumber_CallsEndpointWithVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object, Version.V1);

        await proxy.PayAsync(AnyPaymentRequest);

        // Here we verify we called the right url again
        fakeClient.Verify(x => x.PostAsync<PaymentRequest, ApiResult>(
            //     ^^^^^            
            It.Is<Uri>(t => t.AbsoluteUri.Contains("/v1/pay", StringComparison.InvariantCultureIgnoreCase)), It.IsAny<PaymentRequest>()),
            Times.Once);
    }

    private PaymentRequest AnyPaymentRequest
        => new PaymentRequest
        {
            // All initializations here...
        };
}

These tests rely on Moq to write fakes and object mothers to create test data.

Notice, the Verify() methods in the two tests. Did you notice how buried inside all that boilerplate is the URL we want to check? That’s what we’re interested in. It would be nice if we had a VerifyItCalled() method and we just passed a string or URI with the URL we want.

Let’s create an extension method on top of our fake. Let’s write the VerifyItCalled() method we want. It will receive a relative URL and call Moq Verify() method. Something like this,

using Moq;
using System;

namespace CustomAssertions;

public static class MockApiClientExtensions
{
    public static void VerifyItCalled(this Mock<IApiClient> mock, string relativeUri)
    {
        mock.Verify(x => x.PostAsync<PaymentRequest, ApiResult>(
                        It.Is<Uri>(t => t.AbsoluteUri.Contains(relativeUri, StringComparison.InvariantCultureIgnoreCase)),
                        It.IsAny<PaymentRequest>()),
                    Times.Once);
    }
}

With our VerifyItCalled() in place, let’s refactor our tests to use it,

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System.Threading.Tasks;

namespace CustomAssertions;

[TestClass]
public class PaymentProxyTests
{
    [TestMethod]
    public async Task PayAsync_ByDefault_CallsLatestVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object);

        await proxy.PayAsync(AnyPaymentRequest);

        // Now, it's way more readable
        fakeClient.VerifyItCalled("/v2/pay");
        //         ^^^^^
    }

    [TestMethod]
    public async Task PayAsync_VersionNumber_CallsEndpointWithVersion()
    {
        var fakeClient = new Mock<IApiClient>();
        var proxy = new PaymentProxy(fakeClient.Object, Version.V1);

        await proxy.PayAsync(AnyPaymentRequest);

        // No more boilerplate code to check things
        fakeClient.VerifyItCalled("/v1/pay");
        //         ^^^^^
    }

    private PaymentRequest AnyPaymentRequest
        => new PaymentRequest
        {
            // All initializations here...
        };
}

With our custom Verify() method, our tests are more readable. And, we wrote the tests in the same terms as our domain language. No more ceremony to check we called the right url.

Voilà! That’s how to use custom assertions to write tests in the same terms as your domain model.

In case you have plain assertions, not Verify() methods with Moq, simply write private methods to group your assertions and share them in a base test class. Or write extension methods on the output of the method being tested. For more details on this technique, check xUnitPatterns on Custom Assertions.

If you’re new to fakes, mocks, and stubs, read what are fakes in unit testing and how to write better stubs and mocks. Also, don’t miss my unit testing best practices and the rest of my Unit Testing 101 series where I cover more subjects like this one.

Happy testing!

Let's refactor a test: Payment report

Let’s refactor a test to follow our unit testing best practices. This test is based on a real test I had to modify in one of my client’s projects.

Imagine this test belongs to a payment gateway. The system takes payments on behalf of partners. At the end of every month, the partners get the collected payments discounting any fees. This process is called a payout.

Partners can generate a report with all the transactions associated with a payout. This report can show dates at a different timezone if the user wants it.

This is the test we’re going to refactor. This test is for the GetPayoutDetailsAsync() method. This method finds the payouts in a date range. Then, it shows all transactions related to those payouts. This method feeds a report in Microsoft Excel or any other spreadsheet software.

[TestMethod]
public async Task GetPayoutDetailsAsync_HappyPath_SuccessWithoutTimezone()
{
    var account = TestAccount;

    var payouts = TestPayouts;
    var balanceTransactions = TestBalanceTransactions;
    var payments = TestPayments;

    var request = new PayoutRequest
    {
        PageSize = 10,
        AccountId = "AnyAccountId",
        DateRange = new DateRange
        {
            StartDate = DateTime.Now.AddDays(-15),
            EndDate = DateTime.Now
        }
    };

    var builder = new TypeBuilder<PayoutDetailsService>()
        .WithAccount(account)
        .WithPayouts(payouts)
        .WithBalanceTransactions(balanceTransactions)
        .WithPayments(payments);

    var service = builder.Build();

    var result = await service.GetPayoutDetailsAsync(request);

    Assert.IsTrue(result.Any());

    builder.GetMock<IAccountService>().Verify(s => s.GetAsync(It.IsAny<string>()), Times.Once);
    builder.GetMock<IPayoutWrapper>()
        .Verify(s => s.GetPayoutsByDateRangeAsync(It.IsAny<string>(), It.IsAny<DateRange>()), Times.Once);
    builder.GetMock<IBalanceTransactionWrapper>()
        .Verify(
            s => s.GetBalanceTransactionsByPayoutsAsync(It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<CancellationToken>()), Times.Once);
}

This test uses automocking with TypeBuilder. This TypeBuilder<T> creates an instance of a class with its dependencies replaced by fakes using Moq.

Also, this test uses the Builder pattern to create fakes with some test values before building a new instance. This test relies on object mothers to create input values for the stubs.

1. Separate the Arrange/Act/Assert parts

Let’s start by grouping related code to follow the Arrange/Act/Assert (AAA) principle.

To achieve this, let’s declare variables near their first use and inline the ones used in a single place.

[TestMethod]
public async Task GetPayoutDetailsAsync_HappyPath_SuccessWithoutTimezone()
{
    // Notice we inlined all input variables
    var builder = new TypeBuilder<PayoutDetailsService>()
        .WithAccount(TestAccount)
        .WithPayouts(TestPayouts)
        .WithBalanceTransactions(TestBalanceTransactions)
        .WithPayments(TestPayments);
    var service = builder.Build();

    // Notice we moved the request variable near its first use
    var request = new PayoutRequest
    {
        PageSize = 10,
        AccountId = "AnyAccountId",
        DateRange = new DateRange
        {
            StartDate = DateTime.Now.AddDays(-15),
            EndDate = DateTime.Now
        }
    };
    var result = await service.GetPayoutDetailsAsync(request);

    Assert.IsTrue(result.Any());

    builder.GetMock<IAccountService>().Verify(s => s.GetAsync(It.IsAny<string>()), Times.Once);
    builder.GetMock<IPayoutWrapper>()
        .Verify(s => s.GetPayoutsByDateRangeAsync(It.IsAny<string>(), It.IsAny<DateRange>()), Times.Once);
    builder.GetMock<IBalanceTransactionWrapper>()
        .Verify(
            s => s.GetBalanceTransactionsByPayoutsAsync(It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<CancellationToken>()), Times.Once);
}

Notice we inlined all input variables and move the request variable closer to the GetPayoutDetailsAsync() method where it’s used.

Remember, declare variables near their first use.

2. Show the scenario under test and the expected result

Now, let’s look at the test name.

[TestMethod]
public async Task GetPayoutDetailsAsync_HappyPath_SuccessWithoutTimezone()
{
    // Notice the test name...
    // The rest of the test remains the same,
    // but not for too long
}

It states the GetPayoutDetailsAsync() method should work without a timezone. That’s the scenario of our test.

Let’s follow the “UnitOfWork_Scenario_ExpectedResult” naming convention to show the scenario under test in the middle part of the test name.

Also, let’s avoid the filler word “Success”. In this test, success means the method returns the details without showing the transactions in another timezone. We learned to avoid filler words in our test names when we learned the 4 common mistakes when writing tests.

Let’s rename our test.

[TestMethod]
public async Task GetPayoutDetailsAsync_NoTimeZone_ReturnsDetails()
{
    // Test body remains the same...
}

After this refactoring, it’s a good idea to add another test passing a timezone and checking that the found transactions are in the same timezone.

3. Make test value obvious

In the previous refactor, we renamed our test to show it works without a timezone.

Anyone reading this test should expect a variable named timezone assigned to null or a method WithoutTimeZone() in a builder. Let’s make the test value explicit.

[TestMethod]
public async Task GetPayoutDetailsAsync_NoTimeZone_ReturnsDetails()
{
    var builder = new TypeBuilder<PayoutDetailsService>()
        .WithAccount(TestAccount)
        .WithPayouts(TestPayouts)
        .WithBalanceTransactions(TestBalanceTransactions)
        .WithPayments(TestPayments);
    var service = builder.Build();

    var request = new PayoutRequest
    {
        PageSize = 10,
        AccountId = "AnyAccountId",
        DateRange = new DateRange
        {
            StartDate = DateTime.Now.AddDays(-15),
            EndDate = DateTime.Now
        },
        // Notice we explicitly set no timezone
        TimeZone = null
        //        ^^^^^
    };
    var result = await service.GetPayoutDetailsAsync(request);

    Assert.IsTrue(result.Any());

    builder.GetMock<IAccountService>().Verify(s => s.GetAsync(It.IsAny<string>()), Times.Once);
    builder.GetMock<IPayoutWrapper>()
        .Verify(s => s.GetPayoutsByDateRangeAsync(It.IsAny<string>(), It.IsAny<DateRange>()), Times.Once);
    builder.GetMock<IBalanceTransactionWrapper>()
        .Verify(
            s => s.GetBalanceTransactionsByPayoutsAsync(It.IsAny<string>(), It.IsAny<string>(),
                It.IsAny<CancellationToken>()), Times.Once);
}

If we have more than one test without a timezone, we can use a constant NoTimeZome or an object mother for the PayoutRequest, something like NoTimeZonePayoutRequest.

4. Remove over-specification

For our last refactor, let’s remove those Verify() calls. We don’t need them. We don’t need to assert on stubs.

If any of the stubs weren’t in place, probably we will get a NullReferenceException somewhere in our code. Those extra verifications make our test harder to maintain.

[TestMethod]
public async Task GetPayoutDetailsAsync_NoTimeZone_ReturnsDetails()
{
    var builder = new TypeBuilder<PayoutDetailsService>()
        .WithAccount(TestAccount)
        .WithPayouts(TestPayouts)
        .WithBalanceTransactions(TestBalanceTransactions)
        .WithPayments(TestPayments);
    var service = builder.Build();

    var request = new PayoutRequest
    {
        PageSize = 10,
        AccountId = "AnyAccountId",
        DateRange = new DateRange
        {
            StartDate = DateTime.Now.AddDays(-15),
            EndDate = DateTime.Now
        },
        TimeZone = null
    };
    var result = await service.GetPayoutDetailsAsync(request);

    Assert.IsTrue(result.Any());
    // We stopped verifying on stubs
}

Voilà! That looks better! Unit tests got our back when changing our code. It’s better to keep them clean too. They are our safety net.

If you’re new to unit testing, read Unit Testing 101, 4 common mistakes when writing your first tests and 4 test naming conventions.

For more advanced content on unit testing, check what are fakes in unit testing and don’t miss the rest of my Unit Testing 101 series.

Happy testing!

How to write better assertions in your tests

There’s a lot to say about how to write good unit tests. This time, let’s focus on best practices to write better assertions on our tests.

Here you have 5 tips to write better assertions on your unit tests.

TL;DR

  1. Follow the Arrange/Act/Assert (AAA) pattern
  2. Separate each A of the AAA pattern with line breaks
  3. Don’t put logic in your assertions
  4. Have a single Act and Assert parts in each test
  5. Use the right Assertion methods

1. Follow the Arrange/Act/Assert (AAA) pattern

If you could take home only one thing: follow the Arrange/Act/Assert (AAA) pattern.

The Arrange/Act/Assert (AAA) pattern states that each test should contain three parts: Arrange, Act and Assert.

In the Arrange part, we create classes and input values needed to call the entry point of the code under test.

In the Act part, we call the method to trigger the logic being tested.

In the Assert part, we verify the code under test did what we expected. We check if it returned the right value, threw an exception, or called another component.

For example, let’s bring back one test for Stringie, a (fictional) library to manipulate strings, to show the AAA pattern. Notice how each test has these 3 parts.

For the sake of the example, we have put comments in each AAA part. You don’t need to do that on your own tests.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Stringie.UnitTests;

[TestClass]
public class RemoveTests
{
    [TestMethod]
    public void Remove_ASubstring_RemovesThatSubstring()
    {
        // Arrange
        string str = "Hello, world!";

        // Act
        string transformed = str.Remove("Hello");

        // Assert
        Assert.AreEqual(", world!", transformed);
    }

    [TestMethod]
    public void Remove_NoParameters_ReturnsEmpty()
    {
        // Arrange
        string str = "Hello, world!";

        // Arrange
        string transformed = str.Remove();

        // Arrange
        Assert.AreEqual(0, transformed.Length);
    }
}

ICYMI, we’ve been using Stringie to write our first unit tests in C# and to learn about 4 common mistakes when writing tests.

2. Separate each A of the AAA pattern

Use line breaks to visually separate the AAA parts of each test.

Let’s take a look at the previous tests without line breaks between each AAA part.

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Stringie.UnitTests;

[TestClass]
public class RemoveTests
{
    [TestMethod]
    public void Remove_ASubstring_RemovesThatSubstring()
    {
        string str = "Hello, world!";
        string transformed = str.Remove("Hello");
        Assert.AreEqual(", world!", transformed);
    }

    [TestMethod]
    public void Remove_NoParameters_ReturnsEmpty()
    {
        string str = "Hello, world!";
        string transformed = str.Remove();
        Assert.AreEqual(0, transformed.Length);
    }
}

Not that bad. The larger the tests, the harder it gets.

Have the three AAA parts separated to make your tests easier to read.

In case you’re wondering about those weird method names, they follow one of the most common test naming conventions.

Three paths on the snow
Separate each A of the AAA pattern. Photo by Anne Nygård on Unsplash

3. Don’t put logic in your assertions

Don’t repeat the logic under test in your assertions. And, please, don’t copy the tested logic and paste it into private methods in your test files to use it in your assertions. That’s the most common mistake when writing tests.

Use known, pre-calculated values instead. Declare constants for common expected values.

[TestMethod]
public void Remove_ASubstring_RemovesThatSubstringFromTheEnd()
{
    string str = "Hello, world! Again, Hello";

    string transformed = str.Remove("Hello").From(The.End);

    Assert.AreEqual("Hello, world! Again,", transformed);
    //              ^^^^^
    // Notice how we hardcode an expected value here
}

Notice how we hardcoded an expected value in the Assert part. We didn’t use any other method or copy the logic under test to find the expected substring.

4. Have a single Act and Assert

Have a single Act and Assert parts in your tests. Use parameterized tests to test the same scenario with different test values.

And, don’t put the test values inside an array to loop through it to then assert on each value.

This is a parameterized test with MSTest.

[DataTestMethod]
[DataRow("Hello")]
[DataRow("HELLO")]
[DataRow("HeLlo")]
public void Remove_SubstringWithDifferentCase_RemovesSubstring(string substringToRemove)
{
    var str = "Hello, world!";

    var transformed = str.RemoveAll(substringToRemove).IgnoringCase();

    Assert.AreEqual(", world!", transformed);
}

5. Use the right Assertion methods

Use the right assertion methods of your testing framework. And, don’t roll your own assertion framework.

For example, prefer Assert.IsNull(result); over Assert.AreEqual(null, result);. And, prefer Assert.IsTrue(result) over Assert.AreEqual(true, result);.

When working with strings, prefer StringAssert methods like Contains(), StartsWith() and Matches() instead of exactly comparing two strings. That would make your tests easier to maintain.

Voilà! These are 5 tips to write better assertions. If you want a more complete list of best practices to write your unit tests, check my post Unit Testing Best Practices: A Checklist. And, don’t miss how to write custom assertions to write even more readable tests.

If you’re new to unit testing, start reading Unit Testing 101 and 4 common unit testing mistakes. For more advanced tips, check how to write good unit tests and always write failing tests.

And don’t miss the rest of my Unit Testing 101 series where I cover more subjects like this one.

Happy testing!

Unit Testing Best Practices: A checklist

As part of this series on unit testing, we’ve covered a lot of subjects. From how to write your first unit tests to create test data with Builders to how to write better fakes. I hope I’ve helped you to start writing unit tests or write even better unit tests.

This time, I’m bringing some tips and best practices from my previous posts in one single place for quick reference.

1. On Naming

Choose a naming convention and stick to it.

Every test name should tell the scenario under test and the expected result. Don’t worry about long test names. But don’t name your tests: Test1, Test2, and so on.

Describe in your test names what you’re testing in a language easy to understand, even for non-programmers.

Don’t prefix your test names with “Test.” If you’re using a testing framework that doesn’t need keywords in your test names, don’t do that. With MSTest, there are attributes like [TestClass] and [TestMethod] to mark methods as tests. Other testing frameworks have similar ones.

Don’t use filler words like “Success” or “IsCorrect” in test names. Instead, tell what “success” and “correct” mean for that test. Is it a successful test because it doesn’t throw exceptions? Is it successful because it returns a value different from null? Make your test names easy to understand.

2. On Organization

Make your tests easy to find.

Put your unit tests in a test project named after the project they test. Use the suffix “Tests” or “UnitTests.” For example, if you have a library called MyLibrary, name your test project: MyLibrary.UnitTests.

Put your unit tests separated in files named after the unit of work or entry point of the code you’re testing. Use the suffix “Tests”. For a class MyClass, name your test file: MyClassTests.

Workbench full of tools
Keep your tests organized and easy to find. Photo by Barn Images on Unsplash

3. On Assertions

Follow the Arrange/Act/Assert (AAA) principle.

Separate the body of your tests. Use line breaks to visually separate the three AAA parts in the body of your tests.

Don’t repeat the logic under test in your assertions. And, please, don’t copy the tested logic and paste it into private methods in your test files to use it in your assertions. Use known or pre-calculated values, instead.

Don’t make private methods public to test them. Test private methods when calling your code under test through its public methods.

Have a single Act and Assert parts in your tests. Don’t put test values inside a collection to loop through it and assert on each one. Use parameterized tests to test the same scenario with different test values.

Use the right assertion methods of your testing framework. For example, use Assert.IsNull(result); instead of Assert.AreEqual(null, result);.

Prefer assertion methods for strings like Contains(), StartsWith() and Matches() instead of exactly comparing two strings.

4. On Test Data

Keep the amount of details at the right level

Give enough details to your readers, but not too many to make your tests noisy.

Use factory methods to reduce complex Arrange scenarios.

Make your scenario under test and test values extremely obvious. Don’t make developers decode your tests. Create constants for common test data and expected values.

Use object mothers to create input test values. Have a factory method or property holding a ready-to-use input object. Then, change what you need to match the scenario under test.

Prefer Builders to create complex object graphs. Object mothers are fine if you don’t have lots of variations of the object being constructed. If that’s the case, use the Builder pattern. Compose builders to create complex objects in your tests.

5. On Stubs and Mocks

Write dumb fakes

Use fakes when you depend on external systems you don’t control. Check your code makes the right calls with the right messages.

Avoid complex logic inside your fakes. Don’t add flags to your stubs to return one value or another. Write separate stubs instead.

Don’t write assertions for stubs. Assert on the output of your code under test or use mocks. Remember there’s a difference between stubs and mocks.

Keep one mock per test. Don’t use multiple mocks per test. Write separate tests instead.

Make tests set their own values for fakes. Avoid magic values inside your stubs.

Use descriptive names in your fakes. Name your stubs to indicate the value they return or the exception they throw. For example, ItemOutOfStockStockService and FixedDateClock.

Voilà! Those are my best practices for writing better great unit tests. Don’t forget to always start writing failing tests. And make sure they fail for the right reasons. If you don’t follow Test-Driven Development, comment out some of your code under test or change the assertions on purpose to see your tests failing.

We don’t ship our tests to end users. But it doesn’t mean we shouldn’t care about the quality of our tests. Unit tests got our back when changing our code. They’re our safety net.

Don’t miss the rest of my Unit Testing 101 series where I cover all these subjects in depth.

Ready to upgrade your unit testing skills? Write readable and maintainable unit test with my course Mastering C# Unit Testing with Real-world Examples on Udemy. Learn unit testing best practices while refactoring real unit tests from my past projects. No tests for a Calculator class.

Happy testing!