How to write good unit tests: Use simple test values

Cesar Aguirre - Mar 6 '23 - - Dev Community

I originally posted this post on my blog a couple of weeks ago. It's part of an ongoing series I've been publishing, called Unit Testing 101.

As a default code reviewers for one of my clients, I had to review some code that had one method to merge dictionaries. This is one of the suggestions I gave during that review to write good unit tests. Let's learn how to use simple test values in our unit tests.

To write good unit tests, write the Arrange part of tests using the simplest test values that exercise the scenario under test. Avoid building large object graphs and using magic numbers in the Arrange part of tests.

1. Merging dictionaries

These are two of the unit tests I reviewed. They test the Merge() method.

using MyProject;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;

namespace MyProject.Tests;

[TestClass]
public class DictionaryExtensionsTests
{
    [TestMethod]
    public void Merge_NoDuplicates_DoesNotMergeNullAndEmptyOnes()
    {
        var me = new Dictionary<int, int>
        {
            { 1, 10 }, { 2, 20 }, { 3, 30 }
        };
        var empty = new Dictionary<int, int> { };
        var one = new Dictionary<int, int>
        {
            { 4, 40 }
        };
        var two = new Dictionary<int, int>
        {
            { 5, 50 }, { 6, 60 }, { 7, 70 }, { 8, 80}, { 9, 90 }
        };
        var three = new Dictionary<int, int>
        {
            { 10, 100 }, { 11, 110 }
        };
        var four = new Dictionary<int, int>
        {
            { 12, 120 }, { 13, 130 }, { 14, 140 }, { 15, 150 },
            { 16, 160 }, { 17, 170 }, { 18, 180 }, { 19, 190 }
        };

        var merged = me.Merge(one, empty, null, two, null, three, null, null, four, empty);
        //              πŸ‘†πŸ‘†πŸ‘†

        Assert.AreEqual(19, merged.Keys.Count);
        var keyRange = Enumerable.Range(1, merged.Keys.Count);
        foreach (var entry in merged)
        {
            Assert.IsTrue(keyRange.Contains(entry.Key));
            Assert.AreEqual(entry.Key * 10, entry.Value);
        }
    }

    [TestMethod]
    public void Merge_DuplicateKeys_ReturnNoDuplicates()
    {
        var me = new Dictionary<int, int>
        {
            { 1, 10 }, { 2, 20 }, { 3, 30 }, { 4, 40 },
            { 5, 50 }, { 6, 60 }, { 7, 70 }
        };
        var one = new Dictionary<int, int>
        {
            { 1, 1 }, { 2, 2 }, { 8, 80 }
        };
        var two = new Dictionary<int, int>
        {
            { 3, 3 }, { 9, 90 }
        };
        var three = new Dictionary<int, int>
        {
            { 4, 4 }, { 5, 5 }, { 6, 6 }, { 7, 7 }, { 10, 100 }
        };

        var merged = me.Merge(one, two, three);
        //              πŸ‘†πŸ‘†πŸ‘†

        Assert.AreEqual(10, merged.Keys.Count);
        var keyRange = Enumerable.Range(1, merged.Keys.Count);
        foreach (var entry in merged)
        {
            Assert.IsTrue(keyRange.Contains(entry.Key));
            Assert.AreEqual(entry.Key * 10, entry.Value);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Yes, those are the real tests I had to review. I slightly changed the namespaces and the test names.

2. What's wrong?

Let's take a closer look at the first test. Do we need six dictionaries to test the Merge() method? No! And do we need 19 items? No! We can still cover the same scenario with only two single-item dictionaries without duplicate keys.

Having too many dictionaries with too many items made us write that weird foreach with a multiplication inside. That's why some of the values are multiplied by 10, and others aren't. We don't need that with a simpler scenario.

Unit tests should only have assignments without branching or looping logic.

Looking at the second test, we noticed it followed the same pattern as the first one. Too many items and a weird foreach with a multiplication inside.

pile of English dictionaries

Merging dictionaries...Photo by Justin Case on Unsplash

3. Write tests using simple test values

Let's write our tests using simple test values to prepare our scenario under test.

[TestMethod]
public void Merge_NoDuplicates_DoesNotMergeNullAndEmptyOnes()
{
    var one = new Dictionary<int, int>
    {
        { 1, 10 } // πŸ‘ˆπŸ‘ˆπŸ‘ˆ Just one entry
    };
    var two = new Dictionary<int, int>
    {
        { 2, 20 } // πŸ‘ˆπŸ‘ˆπŸ‘ˆ Just one entry
    };

    var merged = one.Merge(two);
    //               πŸ‘†πŸ‘†πŸ‘†

    Assert.AreEqual(2, merged.Keys.Count);

    Assert.IsTrue(merged.Contains(1));
    Assert.IsTrue(merged.Contains(2));
}

// One test to Merge a dictionary with an empty one
// Another test to Merge a dictionary with a null one

[TestMethod]
public void Merge_DuplicateKeys_ReturnNoDuplicates()
{
    var duplicateKey = 1;
    //  πŸ‘†πŸ‘†πŸ‘†
    var one = new Dictionary<int, int>
    {
        { duplicateKey, 10 }, { 2, 20 }
        //  πŸ‘†πŸ‘†πŸ‘†
    };
    var two = new Dictionary<int, int>
    {
        { duplicateKey, 10 }, { 3, 30 }
        //  πŸ‘†πŸ‘†πŸ‘†
    };
    var merged = one.Merge(two);
    //               πŸ‘†πŸ‘†πŸ‘†

    Assert.AreEqual(3, merged.Keys.Count);

    Assert.IsTrue(merged.Contains(duplicateKey));
    Assert.IsTrue(merged.Contains(2));
    Assert.IsTrue(merged.Contains(3));
}
Enter fullscreen mode Exit fullscreen mode

Our tests are way easier to follow, right?

Notice this time, we boiled down the Arrange part of the first test to only two dictionaries with one item each, without duplicates.

And for the second one, the one for duplicates, we wrote a duplicateKey variable and used it in both dictionaries as key to make the test scenario obvious. This way, after reading the test name, we don't have to decode where the duplicate keys are.

Since we wrote simple tests, we could remove the foreach in the Assert parts and the weird multiplications. We don't need them anymore.

VoilΓ ! That's another tip to write good unit tests. Let's strive to have tests easier to follow with simple test values. Here we used dictionaries, but we can follow this tip when writing integration tests for the database. Often to prepare our test data, we insert multiple records when only one or two are enough to prove our point.


Hey! I'm Cesar, a software engineer and lifelong learner. If you want to upgrade your unit testing skills, check my course: Mastering C# Unit Testing with Real-world Examples on Udemy. Practice with hands-on exercises and learn best practices by refactoring real-world unit tests.

Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .