How to write good unit tests: Noise and Hidden Test Values

These days, I needed to update some unit tests. I found two types of issues with them. Please, continue to read. Maybe, you’re a victim of those issues, too. Let’s learn how to write good unit tests.

To write good unit tests, avoid complex setup scenarios and hidden test values. Often tests are bloated with unneeded or complex code in the Arrange part and full of magic or hidden test values. Unit tests should be even more readable than production code.

The tests I had to update

The tests I needed to update were for an ASP.NET Core API controller, AccountController. This controller created, updated, and suspended user accounts. Also, it sent a welcome email to new users.

These tests checked a configuration object for the sender, reply-to, and contact-us email addresses. The welcome email contained those three emails. If the configuration files miss one of the email addresses, the controller throws an exception from its constructor.

Let’s see one of the tests. This test checks for the sender’s email.

[TestMethod]
public Task AccountController_SenderEmailIsNull_ThrowsException()
{
    var mapper = new Mock<IMapper>();
    var logger = new Mock<ILogger<AccountController>>();
    var accountService = new Mock<IAccountService>();
    var accountPersonService = new Mock<IAccountPersonService>();
    var emailService = new Mock<IEmailService>();
    var emailConfig = new Mock<IOptions<EmailConfiguration>>();
    var httpContextAccessor = new Mock<IHttpContextAccessor>();

    emailConfig.SetupGet(options => options.Value)
        .Returns(new EmailConfiguration()
        {
            ReplyToEmail = "email@email.com",
            SupportEmail = "email@email.com"
        });

    Assert.ThrowsException<ArgumentNullException>(() =>
        new AccountController(
            mapper.Object,
            logger.Object,
            accountService.Object,
            accountPersonService.Object,
            emailService.Object,
            emailConfig.Object,
            httpContextAccessor.Object
        ));

    return Task.CompletedTask;
}

This test uses Moq to create stubs and mocks.

Can you spot any issues in our sample test? The naming convention isn’t one, by the way.

Let’s see the two issues to avoid to write good unit tests.

Adjusting dials on a mixer
Photo by Drew Patrick Miller on Unsplash

1. Reduce the noise

Our sample test only cares about one object: IOptions<EmailConfiguration>. All other objects are noise for our test. They don’t have anything to do with the scenario under test. We have to use them to make our test compile.

Use builder methods to reduce complex setup scenarios.

Let’s reduce the noise from our test with a MakeAccountController() method. It will receive the only parameter the test needs.

After this change, our test looked like this:

[TestMethod]
public void AccountController_SenderEmailIsNull_ThrowsException()
//     ^^^^
// We can make this test a void method
{
    var emailConfig = new Mock<IOptions<EmailConfiguration>>();
    emailConfig
        .SetupGet(options => options.Value)
        .Returns(new EmailConfiguration
        {
            ReplyToEmail = "email@email.com",
            SupportEmail = "email@email.com"
        });

    // Notice how we reduced the noise with a builder
    Assert.ThrowsException<ArgumentNullException>(() =>
        MakeAccountController(emailConfig.Object));
        // ^^^^^
    
    // We don't need a return statement here anymore
}

private AccountController MakeAccountController(IOptions<EmailConfiguration> emailConfiguration)
{
    var mapper = new Mock<IMapper>();
    var logger = new Mock<ILogger<AccountController>>();
    var accountService = new Mock<IAccountService>();
    var accountPersonService = new Mock<IAccountPersonService>();
    var emailService = new Mock<IEmailService>();
    // We don't need Mock<IOptions<EmailConfiguration>> here
    var httpContextAccessor = new Mock<IHttpContextAccessor>();

    return new AccountController(
            mapper.Object,
            logger.Object,
            accountService.Object,
            accountPersonService.Object,
            emailService.Object,
            emailConfiguration,
            // ^^^^^
            httpContextAccessor.Object);
}

Also, since our test doesn’t have any asynchronous code, we could declare our test as a void method and remove the return statement. That looked weird in a unit test, in the first place.

With this refactor, our test started to look simpler and easier to read. Now, it’s clear this test only cares about the EmailConfiguration class.

2. Make your test values obvious

Our test states in its name that the sender’s email is null. Anyone reading this test would expect to see a variable set to null and passed around. But, that’s not the case.

Make scenarios under test and test values extremely obvious.

Please, don’t make developers decode your tests.

To make the test scenario obvious in our example, let’s add SenderEmail = null to the initialization of the EmailConfiguration object.

[TestMethod]
public void AccountController_SenderEmailIsNull_ThrowsException()
{
    var emailConfig = new Mock<IOptions<EmailConfiguration>>();
    emailConfig
        .SetupGet(options => options.Value)
        .Returns(new EmailConfiguration
        {
            // The test value is obvious now
            SenderEmail = null,
            //            ^^^^^
            ReplyToEmail = "email@email.com",
            SupportEmail = "email@email.com"
        });

    Assert.ThrowsException<ArgumentNullException>(() =>
        MakeAccountController(emailConfig.Object));
}

If we have similar scenarios, we can use a constant like const string NoEmail = null. Or prefer object mothers and builders to create test data.

3. Don’t write mocks for IOptions

Finally, as an aside, we don’t need a mock on IOptions<EmailConfiguration>.

Don’t use a mock or a stub with the IOptions interface. That would introduce extra complexity. Use Options.Create() with the value to configure instead.

Let’s use the Option.Create() method instead.

[TestMethod]
public void AccountController_SenderEmailIsNull_ThrowsException()
{
    var emailConfig = Options.Create(new EmailConfiguration
    //                ^^^^^
    {
        SenderEmail = null,
        ReplyToEmail = "email@email.com",
        SupportEmail = "email@email.com"
    });

    Assert.ThrowsException<ArgumentNullException>(() =>
        MakeAccountController(emailConfig));
}

Voilà! That’s way easier to read. Do you have noise and hidden test values in your tests? Remember, readability is one of the pillars of unit testing. Don’t make developers decode your tests.

For other tips on writing good unit tests, check my follow-ups on writing failing tests first and using simple test values. Also, don’t miss my Unit Testing 101 series where I cover more subjects like this.

Happy unit testing!

Let's React. Learn React in 30 days

Do you want to learn React and you don’t where to start? Don’t look for any other curated list of resources. Let’s learn React in 30 days!

React is a JavaScript library to build user interfaces. It doesn’t do a lot of things. It renders elements on the screen. Period! React isn’t a Swiss-army knife framework full of functionalities.

To learn React in 30 days, start learning to create components and understand the difference between props and state. Next, learn about hooks and how to style components. After that, learn about managing state with hooks. But don’t rush to use Redux from the beginning.

Building a rocket ship
Building a rocket ship. Photo by Kelly Sikkema on Unsplash

Instead of reading books from cover to cover or passively watching YouTube videos, let’s learn React by doing, by getting our hands dirty. Let’s recreate examples, build mini-projects, and stop copy-pasting. If you’re instered in learning strategies, check my takeaways from the Ultralearning book.

These are some resources to learn React, its prerequisites, and related subjects.

1. Prerequisites

Before starting to work with React, make sure to know about flexbox in CSS and ES6 JavaScript features.

CSS

JavaScript

Some of these projects include the backend side using Node.js. You can find more vanilla projects without any backend code on 15 Vanilla Project Ideas and 20+ Web Projects With Vanilla JavaScript.

Don’t mess your environment with different versions of Node. Follow Don’t Install Node Until You’ve Read This and Your Development Workflow Just Got Better, With Docker Compose.

2. React

Study Plans

React: First steps

React Hooks

Walk-throughs

Basic & Intermediate

Advanced

3. React and Redux

Redux could be the most challenging subject. You have to learn new concepts like: store, actions, reducers, thunks, sagas, dispatching.

Before getting into Redux, start by learning how to use useState hook, then useReducer, then useContext, and last Redux. It feels more natural this way.

Make sure to understand what to put into a Redux store and where you should make your API calls. Be aware you might not need Redux at all.

Tutorials

Walk-throughs

4. Courses

Free

5. Practice and Project Ideas

6. Other resources

Voilà! Those are the resources and project ideas to learn React in 30 days. Start small creating components and understanding the difference between props and states. You can find my own 30-day journey following the resources from this post in my GitHub LetsReact repository.

canro91/LetsReact - GitHub

If you’re interested in learning projects, check Let’s Go and my advice to start an ultralearning project.

Happy coding!

Three Language Lessons I Learned on my First Visit to France

This post is about one of my hobbies: learning new languages. But not programming languages. Foreign languages.

I want to share three lessons I learned while traveling to France to practice my French-speaking skills. Each lesson is behind a funny story that happened on my first visit to France.

Three language lessons I learned in my First visit to France
Photo by Byeong woo Kang on Unsplash

These are the three lessons I learned:

  1. Don’t Fall into the temptation of speaking English. Keep speaking in your target language when locals talk to you in English. Don’t think they’re rude or you aren’t “worthy” of their language. They want to practice too. Ask politely to continue speaking in your target language. Or pretend you don’t speak English.
  2. Learn vocabulary to suit your needs. If you’re traveling for work, vacations, or cultural exchange, you will need vocabulary for totally different situations. I learned this lesson while waiting at the security at an airport. Can you imagine what happened?
  3. Mistakes are progress. Embrace it when locals correct your language skills. Don’t feel discouraged when locals correct your mistakes. Imagine you got a free language lesson. Probably you won’t make that mistake again.

To read the full story, go to “Fluent in 3 Months” at 3 Language Lessons I Learned on my First Visit to France where I originally published it.

Happy learning!

TIL: NULL isn't LIKE anything else in SQL Server

How does the LIKE operator handle NULL values of a column? Let’s see what SQL Server does when using LIKE with a nullable column.

When using the LIKE operator on a nullable column, SQL Server doesn’t include in the results rows with NULL values in that column. The same is true, when using NOT LIKE in a WHERE clause.

Let’s see an example. Let’s create a Client table with an ID, name and middleName. Only two of the four sample clients have a middlename.

CREATE TABLE #Clients
(
    ID INT,
    Name VARCHAR(20),
    MiddleName VARCHAR(20)
)
GO

INSERT INTO #Clients
VALUES
    (1, 'Alice', 'A'),
    (2, 'Bob',   NULL),
    (3, 'Charlie', 'C'),
    (4, 'Dwight',  NULL)
GO

Let’s find all users with middlename starting and not starting with ‘A’.

SELECT *
FROM #Clients
WHERE MiddleName LIKE 'A%'
GO

SELECT *
FROM #Clients
WHERE MiddleName NOT LIKE 'A%'
GO

Notice the results don’t include any rows with NULL middlenames.

Results of querying a nullable column with LIKE
Results of querying a nullable column with LIKE and NOT LIKE

Voilà! That’s how SQL Server handle NULL when using LIKE and NOT LIKE. Remember you don’t need to check for null values.

If you want to read more SQL Server content, check six performance tuning tips and the lessons learned while tuning a store procedure to search reservations.

Source: NULL is NOT LIKE and NOT NOT LIKE

BugOfTheDay: How I tuned a procedure to find reservations

This time, one of the searching features for reservations was timing out. The appropiate store procedure took ~5 minutes to finish. This is how I tuned it.

To tune a store procedure, start by looking for expensive operators in its Actual Execution plan. Reduce the number of joining tables and stay away from common bad practices like putting functions around columns in WHERE clauses.

After opening the actual exection plan with SentryOne Plan Explorer, the most-CPU expensive and slowest statement looked like this:

DELETE res
FROM #resTemp res
WHERE reservationID NOT IN (
        SELECT res1.reservationID
        FROM #resTemp res1
        JOIN dbo.reservations res
            ON res.reservationID = res1.reservationID
        JOIN dbo.accounts a
            ON (a.accountID = res.accountID
                  OR a.accountID = res.columnWithAccountID
                  OR a.accountID = res.columnWithAccountIDToo)
                AND a.clientID = @clientID
        WHERE ISNULL(a.accountNumber, '') + ISNULL(a.accountNumberAlpha, '') LIKE @accountNumber + '%'
        );

This query belonged to a store procedure to search reservations by a bunch of filters. Among its filters, a hotelier can find all reservations assigned to a client’s internal account number.

From the above query, the #resTemp table had reservations from previous queries in the same store procedure. The DELETE statement removes all reservations without the given account number.

Inside SQL Server Management Studio, the store procedure did about 193 millions of logical reads to the dbo.accounts table. That’s a lot!

For SQL Server, logical reads are the number of 8KB pages that SQL Server has to read to execute a query. Generally, the fewer logical reads, the faster a query runs.

1. Remove extra joins

The subquery in the DELETE joined the found reservations with the dbo.reservations table. And then, it joined the dbo.accounts table checking for any of the three columns with an accountID. Yes, a reservation could have an accountID in three columns in the same table. Don’t ask me why.

This subquery performed an Index Scan on the dbo.reservations table. It had a couple of millions of records. That’s the main table in any Reservation Management System.

To remove the extra join to the dbo.reservations table in the subquery, I added the three referenced columns (accountID, columnWithAccountID, columnWithAccountIDToo) inside the ON joining the dbo.accounts to the #resTemp temporary table. By the way, those aren’t the real names of those columns.

After this change, the store procedure took ~8 seconds. It read about 165,000 pages for the dbo.accounts table. Wow!

DELETE res
FROM #resTemp res
WHERE reservationID NOT IN (
        SELECT res1.reservationID
        FROM #resTemp res1
        /* We don't need the extra JOIN here */
        INNER JOIN dbo.accounts a
            ON (a.accountID = res1.accountID
                  OR a.accountID = res1.columnWithAccountID
                  OR a.accountID = res1.columnWithAccountIDToo)
                AND a.clientID = @clientID
        WHERE ISNULL(a.accountNumber, '') + ISNULL(a.accountNumberAlpha, '') LIKE @accountNumber + '%'
        );

2. Use NOT EXISTS

Then, instead of NOT IN, I used NOT EXISTS. This way, I could lead the subquery from the dbo.accounts table. Another JOIN gone!

After this change, the store procedure finished in about 5 seconds.

DELETE res
FROM #resTemp res
WHERE NOT EXISTS (
        SELECT 1/0
        /* Again, we got rid of another JOIN */
        FROM dbo.accounts a
        WHERE (a.accountID = res.accountID
                OR a.accountID = res.columnWithAccountID
                OR a.accountID = res.columnWithAccountIDToo)
            AND a.clientID = @clientID
            AND ISNULL(a.accountNumber, '') + ISNULL(a.accountNumberAlpha, '') LIKE @accountNumber + '%'
        );

Those ~4-5 seconds were good enough. But, there was still room for improvement.

If you're wondering about that weird SELECT 1/0, check my post on EXISTS SELECT in SQL Server

3. Don’t use functions in WHERE’s

The ISNULL() functions in the WHERE look weird. Using functions around columns in WHERE clauses is a common anti-pattern.

In this case, a computed column concatenating the two parts of account numbers would help. Yes, account numbers were stored splitted into two columns. Again, don’t ask me why.

ALTER TABLE dbo.accounts
    ADD AccountNumberComplete
    AS ISNULL(accountNumber, '') + ISNULL(accountNumberAlpha, '');

I didn’t use a persisted column. The dbo.accounts table was a huge table, creating a persisted columns would have required scanning the whole table. I only wanted SQL Server to have better statistics to run the DELETE statement.

To take things even further, an index leading on the ClientId followed by that computed column could make things even faster.

CREATE INDEX ClientID_AccountNumberComplete
    ON dbo.accounts(ClientID, AccountNumberComplete);

I didn’t need to include the accountId on the index definition since it was the primary key of the table.

Voilà! That’s how I tuned this query. The lesson to take home is to reduce the number of joining tables and stay away from functions in your WHERE’s. Often, a computed column can help SQL Server to run queries with functions in the WHERE clause. Even, without rewriting the query to use the new computed column.

For more content about SQL Server, check Six SQL Server tuning tips and Two free tools to format your SQL queries.

Happy coding!