My Top 16 newest C# features by version

C# is a language in constant evolution. It has changed a lot since its initial versions in the early 2000’s. Every version brings new features to write more concise and readable code. These are some C# features I like the most and use often. Hope you find them useful too.

Let’s start with the best C# features by version, starting from version 6.

C# 6.0

String interpolation: $”Hello, {name}”

Before with string.Format(), we could miss a parameter or add them in the wrong order. If we forgot a parameter, we will get a FormatException.

With string interpolation, we can inline variables directly in the string we want to build. To use string interpolation, before the opening quote of your string, let’s add $ and wrap our variables around {}.

Before with string.Format(),

string.Format("Hello, {0} {1}", title, name);

But, if we forgot to add one parameter,

string.Format("Hello, {0} {1}", title/*, I forgot to add the name parameter*/);
// ^^^
// System.FormatException:
//    Index (zero based) must be greater than or equal to zero and less than the size of the argument list.

After with string interpolation,

$"Hello, {title} {name}";

Now, it’s clearer if we’re missing a parameter or if we have them in the wrong order.

Null-conditional (?.) and null-coalescing operators (??)

Starting from C# 6.0, we have two new operators: null-conditional ?. and null-coalescing ?? operators. These two new operators helps us to get rid of null values and NullReferenceException.

With the null-conditional ?. operator, we access a member’s object if the object isn’t null. Otherwise, it returns null.

The null-coalescing ?? operator evaluates an alternative expression if the first one is null.

Before,

string name = ReadNameFromSomewhere();
if (name == null)
{
    name = "none";
}
else
{
    name.Trim();
}

After,

string name = ReadNameFromSomewhere();
name?.Trim() ?? "none";

It executes Trim() only if name isn’t null. Otherwise, name?.Trim() returns null. But, with the ?? operator, the whole expression returns “none”.

Expression body definition (=>)

Now, one-line functions are truly one liners. We can use => to declare the body of methods and properties in a single line of code.

Before,

public int MeaningOfLife()
{
    return 42;
}

After,

public int MeaningOfLife()
    => 42;

nameof expression

As its name implies, the nameof operator returns the name of a variable, type or member as a string. It makes renaming things easier.

Before without nameof,

public void SomeMethod(string param1)
{
    if (string.IsNullOrEmpty(param1))
        throw new ArgumentNullException("param1");
}

After with nameof,

public void SomeMethod(string param1)
{
    if (string.IsNullOrEmpty(param1))
        throw new ArgumentNullException(nameof(param1));
}

C# 7.X

Throw expressions

Now, throws are expressions. It means we can use them inside conditionals and null coalescing expressions.

We can combine the ??, throw and nameof operators to check required parameters inside constructors. For example,

public class Movie
{
    private readonly string _title;
    private readonly Director _director;

    public Movie(string title, Director director)
    {
        _title = title;
        _director = director ?? throw new ArgumentNullException(nameof(director));
    }
}

Notice, how the ?? operator evaluates the expression on the right, which is a throw.

new Movie("Titanic", null);
// ^^^
// System.ArgumentNullException: Value cannot be null.
// Parameter name: director

out variables

We can inline the variable declaration next to the out keyword using the var keyword.

Before, we had to declare a variable in a separate statement,

int count = 0;
int.TryParse(readFromKey, out count);

After, inlining the variable declaration,

int.TryParse(readFromKey, out var count);

Instead of declaring a variable, we can use discards _ to ignore the output value. For example,

int.TryParse(readFromKey, out _);

I’m not a big fan of methods with out references. But, with this feature I like them a bit more. I prefer tuples.

Tuples

Speaking of tuples…Now we can access tuple members by name. We don’t need to use Item1 or Item2 anymore.

We can declare tuples wrapping its members inside parenthesis. For example, to declare a pair of coordinates, it would be (int X, int Y) origin = (0, 0).

We can use named members when declaring methods and deconstructing returned values.

Before,

Tuple<string, string> Greet() { }

var greeting = Greet();
var name = greeting.Item1;

After,

(string Salutation, string Name) Greet() { }

var greeting = Greet();
var name = greeting.Name;

Even better,

(string Salutation, string Name) Greet() { }

var (Salutation, Name) = Greet();

Do you remember discards? We can use them with tuples too.

(_, string name) = Greet();

Asynchronous Main methods

Now, async Main methods are available in Console applications.

Before,

public static int Main(string[] args)
{
    return DoSomethingAsync().GetAwaiter().GetResult();
}

After,

public static async Task<int> Main(string[] args)
{
    await DoSomethingAsync();
}

Pattern matching

With pattern matching, we have more flexibility in control flow structures like switch and if. Let’s see a couple of examples.

On one hand, we can avoid casting types inside if statements.

Before without pattern matching, we needed to cast types,

var employee = CreateEmployee();
if (employee is SalaryEmployee)
{
    var salaryEmployee = (SalaryEmployee)employee;
    DoSomething(salaryEmployee);
}

After, with pattern matching, we can declare a variable in the condition,

if (employee is SalaryEmployee salaryEmployee)
{
    DoSomething(salaryEmployee);
}

On another hand, we can use a when clause inside switch.

Before, we had to rely on if statements inside the same case, like this

var employee = CreateEmployee();
switch (employee)
{
    case SalaryEmployee salaryEmployee:
        if (salaryEmployee.Salary > 1000)
        {
            DoSomething(salaryEmployee);
        }
        else
        {
            DoSomethingElse(salaryEmployee);
        }
        break;

    // other cases...        
}

Now, with pattern matching, we can have separate cases,

var employee = CreateEmployee();
switch (employee)
{
    case SalaryEmployee salaryEmployee when salaryEmployee.Salary > 1000:
        DoSomething(salaryEmployee);
        break;

    case SalaryEmployee salaryEmployee:
        DoSomethingElse(salaryEmployee);
        break;

    // other cases...
}

I found it more readable this way. Let’s keep the conditional case before the one without conditions.

Tools on a workbench
Photo by Oxa Roxa on Unsplash

C# 8.0

switch expressions

Speaking of switch statements, starting from C# 8.0 switch are expressions. It means we can assign a switch to a variable or return a switch from a method.

Before a switch looked like this one,

CardType cardType;

switch (cardBrand)
{
    case "Visa":
        cardType = CardType.Visa;
        break;

    case "MasterCard":
        cardType = CardType.MasterCard;
        break;
        
    case "American Express":
        cardType = CardType.AmericanExpress;
        break;
        
    default:
        throw new ArgumentException(cardBrand);
}

After with switch as expressions,

CardType cardType = cardBrand switch
{
    "Visa" => CardType.Visa,
    "MasterCard" => CardType.MasterCard,
    "American Express" => CardType.AmericanExpress,
    _ => throw new ArgumentException(cardBrand)
};

Switch expressions are more compact, right? Did you notice we assigned the result of the switch to the cardType variable? Cool!

Indices and ranges

If you have used negative indices in Python, you would find this feature familiar. In Python, we use negative indices to reference elements from the end of lists.

We have a similar feature in C#, not with negative indices, but with the index from end operator, ^.

With the index from end operator, the last element of an array would be array[^1].

Before, we had to substract from the length of the array to access an element from the end. The last element of an array was array[array.Length - 1].

var helloWorld = new string[] { "Hello", ", ", "world!" };
helloWorld[helloWorld.Length - 1]; // "world!"

After, with the index from end operator,

var helloWorld = new string[] { "Hello", ", ", "world!" };
helloWorld[^1]; // "world!"

In the same spirit, we have ranges. An array without its last element would be array[0..^1]

var helloWorld = new string[] { "Hello", ", ", "world!" };
string.Join("", helloWorld[0..^1]); // Hello,

Null-coalescing assignment (??=)

Do you remember the ?. and ?? operators? Now, there is another operator to work with null. The null-coalescing assignment operator, ??=. It only assigns a variable if its value isn’t null.

Before,

int? magicNumber = ReadMagicNumberFromSomewhere();

if (magicNumber == null)
    magicNumber = 7;

After,

int? magicNumber = ReadMagicNumberFromSomewhere();

magicNumber ??= 7;

Using declarations

A variable preceded by using is disposed at the end of the scope. We can get rid of the parethensis around using statements and the brackets wrapping its body.

Before,

using (var reader = new StreamReader(fileName))
{
    string line; 
    while ((line = reader.ReadLine()) != null)  
    {  
        // Do something  
    }  
}

After,

using var reader = new StreamReader(fileName);
string line; 
while ((line = reader.ReadLine()) != null)  
{  
    // Do something  
}

Nullable reference types

With C# 8.0, all reference variables are non-nullable by default. Any attempt to dereference a nullable reference gets a warning from the compiler. Goodbye, NullReferenceException!

To declare a variable that can be null, we need to add to its type declaration an ?. The same way we have always declared nullable value types like int?. For example, a nullable string would be string? canBeNull;

This is a breaking change. We need to turn on this feature at the project level. To do so, let’s add <Nullable>enable</Nullable> inside the PropertyGroup in our csproj files.

For a console application, the csproj file with this feature turned on look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Before, if we access a member of a null reference, we get a NullReferenceException.

string name = null;
SayHi(name); // <- System.NullReferenceException
// ^^^
// System.NullReferenceException: 'Object reference not set to an instance of an object.'
// name was null.

void SayHi(string name)
  => Console.WriteLine(name.Trim());

But now, we get a compiler warning,

string name = null;
// ^^^^^
// warning CS8600: Converting null literal or possible null value to non-nullable type.

string? canBeNullName = null;
SayHi(name);
// ^^^^^
// warning CS8604: Possible null reference argument for parameter 'name'

To get rid of the compiler warning, we have to check for null values first.

string? canBeNullName = null;
if (canBeNullName != null)
{
    SayHi(name);
}

C# 9.0

These are two features from C# 9.0 I haven’t adopted yet, but I found interesting.

Records

A record is an immutable reference type with built-in equality methods. When we create a record, the compiler creates a ToString method, a value-based equality methods and other methods for us.

Records are helpful to replace value-objects in our code.

public record Movie
{
    public string Title { get; }
    public int ReleaseYear { get; }
    
    public Movie(string title, int releaseYear) => (Title, ReleaseYear) = (title, releaseYear);
}

Top-level statements

All the boilerplate code is now gone from Main methods. It gets closer to scripting languages like Python and Ruby.

Before, to write the “Hello, world!” program in C#, we needed to bring namespaces, classes, methods and arrays just to print a message out to the console.

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

After, it boils down to just two lines.

using System;

Console.WriteLine("Hello World!");

Voilà! These are the C# features I like the most. Which ones didn’t you know about? Which ones you use most often? What features would you like to see in future versions?

Do you want to learn more about the C# language? Check my C# Definitive Guide. It contains the subjects I believe all intermediate C# developers should know. Are you new to LINQ? Check my quick guide to LINQ with examples. And, don’t miss my C# idioms series.

Happy coding!

Monday Links: Farmers, Incidents & Holmes

Today I want to start a new series: Monday Links. I want to share interesting and worth-sharing blog posts, articles or anything I read online in the last month or two. You may find them interesting and instructive too.

Farmers always Worked From Home

We’re in the work-from-home era. But, farmers have always worked from home. This article states we all got the work-life balance wrong. Speaking about a farmer neighbor…“He does what he needs to, and rests in between”. Read full article

An Old Hacker’s Tips On Staying Employed

An ode to personal brand. Build a reputation that will take of you on rough times. The “Two and Done” principle was my favorite. When making a decision, present your case only twice. After that, say that you have outlined your options and you want to move on taking somebody else’s idea. Read full article

MOCR 2 during the Apollo 13 crisis
Houston, we have a problem! Photo by NASA, Public Domain

Secrets of great incident management

Incident commander, driver, communicator, recorder, researcher…It reminds me one of those movies about space missions. “Houston, we have a problem!” Outages are stressful situations for sure. People running, pointing fingers, making phone calls…the worst thing is when only you figure out you have an outage when you get a phone call from one of your clients. Arrggg! Read full article

Patterns in confusing explanations

It explains how not to explain things. I felt guilty of doing some of these anti-patterns. The ones about analogies, ignoring the whys…One piece of advise I liked was to write for one person in mind. Imagine a friend or coworker and explain the subject to her. Read full article

What Programmers Can Learn From Sherlock Holmes

What Sherlock Home stories and programming have in common. Not all facts are obvious to everyone. We all have one Lestrade in our programmer’s life. I liked those two things. Read full article

Voilà! These were my most favorites reads from the last couple of months. See you in a month or two in the next Monday Links! In the mean time, stay tune to my Unit Testing 101 series to learn from how to write your first unit tests with MSTest to how to write custom assertions.

Happy reading!

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 if you’re a seasoned developer new to unit testing, this is the place for you.

Write it

Write your first unit tests with MSTest It’s the starting point to write unit tests. No prerequisites needed.

Identify and fix these 4 common mistakes when writing your first unit tests. Learn one of these 4 naming conventions and stick to it.

To find these first three posts plus a summary of “The Art of Unit Testing” and my best tips and advice on the subject, grab a copy of my free ebook “Unit Testing 101”. Click on the image below to download.

Grab your own copy of Unit Testing 101

Improve it

How to write good unit tests shows two common issues when writing unit tests: complex setup scenarios and hidden test values.

Make sure to always write a failing test first.

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

Fake it

Learn what fakes are in unit testing. It shows 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.

Last but not least, read all tips of this series on Unit Testing Best Practices. As an example, see how to refactor a real-world test to follow some of those best practices. Deep into assertions, check how to write better assertions and how to write custom assertions.

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

canro91/Testing101 - GitHub

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 in our tests.

Use custom assertions to encapsulate multiple assertions on a single method and to express assertions 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 fakes objects.

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 endpoints. If we change the client version number, we should include the version number in the endpoint url.

We could write some unit test like the one below. These tests rely on Moq to write fakes. And, on object mothers to create test data.

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>(), null, null), 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>(), null, null), Times.Once);
        }

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

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 with the url we wanted.

Let’s write a custom assertion for that.

Shaving wood
Photo by Mike Kenneally on Unsplash

Write a custom assertion

Write custom assertions to group related assertions on a single method and to express assertions in the same language of the domain model.

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

Custom MSTest Assert methods

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.

Here, we’re creating a StringIsEmpty() method.

internal 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.

Assert.That.StringIsEmpty("");

Wit this new Assert method, we can 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.

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);
        }
    }
}

Custom Verify method

If we’re using a mocking library, we can create our custom Verify() methods.

For our API client example, let’s create an extesion 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 the method below.

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 way more readable
            fakeClient.VerifyItCalled("/v2/pay");
        }

        [TestMethod]
        public async Task PayAsync_ByDefault_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 containing your regular methods from Assert class and share them in a base test class. Or write extensions 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 how to write better assertions.

Happy testing!

Unit Testing Best Practices: Let's refactor a test

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 in 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 find 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 in object mothers to create input values for the stubs.

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 its 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 its first use.

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 tests 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 refactor, it’s a good idea to add another test passing a timezone and checking that the found transactions are in the same timezone.

Make test value obvious

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

Anyone reading this test should expected 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 timezone, we can use a constant NoTimeZome or an object mother for the PayoutRequest, something like NoTimeZonePayoutRequest.

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, how to write fakes with Moq and these tips to write better stubs and mocks.

Happy testing!