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
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
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 the first posts from this series 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.
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.
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.
Want to write readable and maintainable unit tests in C#? Join my course Mastering C# Unit Testing with Real-world Examples on Udemy and learn unit testing best practices while refactoring real unit tests from my past projects. No more tests for a Calculator class.
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.
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,
publicstaticclassCustomAssert{publicstaticvoidStringIsEmpty(thisAssertassert,stringactual){if(string.IsNullOrEmpty(actual)){return;}thrownewAssertFailedException($"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,
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,
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;usingSystem;usingSystem.Threading.Tasks;namespaceCustomAssertions;[TestClass]publicclassPaymentProxyTests{[TestMethod]publicasyncTaskPayAsync_ByDefault_CallsLatestVersion(){varfakeClient=newMock<IApiClient>();varproxy=newPaymentProxy(fakeClient.Object);awaitproxy.PayAsync(AnyPaymentRequest);// Here we verify we called the right urlfakeClient.Verify(x=>x.PostAsync<PaymentRequest,ApiResult>(// ^^^^^It.Is<Uri>(t=>t.AbsoluteUri.Contains("/v2/pay",StringComparison.InvariantCultureIgnoreCase)),It.IsAny<PaymentRequest>()),Times.Once);}[TestMethod]publicasyncTaskPayAsync_VersionNumber_CallsEndpointWithVersion(){varfakeClient=newMock<IApiClient>();varproxy=newPaymentProxy(fakeClient.Object,Version.V1);awaitproxy.PayAsync(AnyPaymentRequest);// Here we verify we called the right url againfakeClient.Verify(x=>x.PostAsync<PaymentRequest,ApiResult>(// ^^^^^ It.Is<Uri>(t=>t.AbsoluteUri.Contains("/v1/pay",StringComparison.InvariantCultureIgnoreCase)),It.IsAny<PaymentRequest>()),Times.Once);}privatePaymentRequestAnyPaymentRequest=>newPaymentRequest{// 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 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,
With our VerifyItCalled() in place, let’s refactor our tests to use it,
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;usingSystem.Threading.Tasks;namespaceCustomAssertions;[TestClass]publicclassPaymentProxyTests{[TestMethod]publicasyncTaskPayAsync_ByDefault_CallsLatestVersion(){varfakeClient=newMock<IApiClient>();varproxy=newPaymentProxy(fakeClient.Object);awaitproxy.PayAsync(AnyPaymentRequest);// Now, it's way more readablefakeClient.VerifyItCalled("/v2/pay");// ^^^^^}[TestMethod]publicasyncTaskPayAsync_VersionNumber_CallsEndpointWithVersion(){varfakeClient=newMock<IApiClient>();varproxy=newPaymentProxy(fakeClient.Object,Version.V1);awaitproxy.PayAsync(AnyPaymentRequest);// No more boilerplate code to check thingsfakeClient.VerifyItCalled("/v1/pay");// ^^^^^}privatePaymentRequestAnyPaymentRequest=>newPaymentRequest{// 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.
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.
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.
To achieve this, let’s declare variables near their first use and inline the ones used in a single place.
[TestMethod]publicasyncTaskGetPayoutDetailsAsync_HappyPath_SuccessWithoutTimezone(){// Notice we inlined all input variablesvarbuilder=newTypeBuilder<PayoutDetailsService>().WithAccount(TestAccount).WithPayouts(TestPayouts).WithBalanceTransactions(TestBalanceTransactions).WithPayments(TestPayments);varservice=builder.Build();// Notice we moved the request variable near its first usevarrequest=newPayoutRequest{PageSize=10,AccountId="AnyAccountId",DateRange=newDateRange{StartDate=DateTime.Now.AddDays(-15),EndDate=DateTime.Now}};varresult=awaitservice.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]publicasyncTaskGetPayoutDetailsAsync_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.
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]publicasyncTaskGetPayoutDetailsAsync_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]publicasyncTaskGetPayoutDetailsAsync_NoTimeZone_ReturnsDetails(){varbuilder=newTypeBuilder<PayoutDetailsService>().WithAccount(TestAccount).WithPayouts(TestPayouts).WithBalanceTransactions(TestBalanceTransactions).WithPayments(TestPayments);varservice=builder.Build();varrequest=newPayoutRequest{PageSize=10,AccountId="AnyAccountId",DateRange=newDateRange{StartDate=DateTime.Now.AddDays(-15),EndDate=DateTime.Now},// Notice we explicitly set no timezoneTimeZone=null// ^^^^^};varresult=awaitservice.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.
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]publicasyncTaskGetPayoutDetailsAsync_NoTimeZone_ReturnsDetails(){varbuilder=newTypeBuilder<PayoutDetailsService>().WithAccount(TestAccount).WithPayouts(TestPayouts).WithBalanceTransactions(TestBalanceTransactions).WithPayments(TestPayments);varservice=builder.Build();varrequest=newPayoutRequest{PageSize=10,AccountId="AnyAccountId",DateRange=newDateRange{StartDate=DateTime.Now.AddDays(-15),EndDate=DateTime.Now},TimeZone=null};varresult=awaitservice.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.
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
Follow the Arrange/Act/Assert (AAA) pattern
Separate each A of the AAA pattern with line breaks
Don’t put logic in your assertions
Have a single Act and Assert parts in each test
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.
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]publicvoidRemove_ASubstring_RemovesThatSubstringFromTheEnd(){stringstr="Hello, world! Again, Hello";stringtransformed=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.
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.