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.
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.
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.
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.
Writing tests for services with lots of collaborators can be tedious. I know! We will end up with complex Arrange parts and lots of fakes. Let’s see three alternatives to write simpler tests with builder methods, Type Builders and AutoFixture.
To write simpler tests for services with lots of collaborators, use builder methods to create only the fakes needed in every test. As an alternative, use auto-mocking to create a service with its collaborators replaced by test doubles.
To show these three alternatives, let’s bring back our OrderService class. We used it to show the difference between stubs and mocks. Again, the OrderService checks if an item has stock available to then charge a credit card.
This time, let’s add an IDeliveryService to create a shipment order and an IOrderRepository to keep track of order status. With these two changes, our OrderService will look like this:
publicclassOrderService{privatereadonlyIPaymentGateway_paymentGateway;privatereadonlyIStockService_stockService;privatereadonlyIDeliveryService_deliveryService;privatereadonlyIOrderRepository_orderRepository;publicOrderService(IPaymentGatewaypaymentGateway,IStockServicestockService,IDeliveryServicedeliveryService,IOrderRepositoryorderRepository){_paymentGateway=paymentGateway;_stockService=stockService;_deliveryService=deliveryService;_orderRepository=orderRepository;}publicOrderResultPlaceOrder(Orderorder){if(!_stockService.IsStockAvailable(order)){thrownewOutOfStockException();}// Process payment, ship items, and store order status...returnnewPlaceOrderResult(order);}}
I know, I know! We could argue our OrderService is doing a lot of things! Bear with me.
Let’s write a test to check if the payment gateway is called when we place an order. We’re using Moq to write fakes. This test will look like this:
Sometimes, we need to create fakes for our collaborators even when the behavior under test doesn’t need them.
1. Builder methods
One easy alternative to writing simpler tests is to use builder methods.
With a builder method, we only create the fakes we need inside our tests. And, inside the builder method, we create “empty” fakes for the collaborators we don’t need for the tested scenario.
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;namespaceWithABuilderMethod;[TestClass]publicclassOrderServiceTestsBuilder{[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGateway(){varstockService=newMock<IStockService>();stockService.Setup(t=>t.IsStockAvailable(It.IsAny<Order>())).Returns(true);varpaymentGateway=newMock<IPaymentGateway>();varorderService=MakeOrderService(stockService.Object,paymentGateway.Object);// ^^^^^// We add a new MakeOrderService methodvarorder=newOrder();orderService.PlaceOrder(order);paymentGateway.Verify(t=>t.ProcessPayment(order));}privateOrderServiceMakeOrderService(IStockServicestockService,IPaymentGatewaypaymentGateway)// ^^^^^// Notice we only pass the fakes we need{vardeliveryService=newMock<IDeliveryService>();varorderRepository=newMock<IOrderRepository>();varservice=newOrderService(paymentGateway,stockService,deliveryService.Object,orderRepository.Object);returnservice;}}
With the MakeOrderService() method, we only deal with the mocks we care about in our test: the ones for IStockService and IPaymentService.
Builder methods are fine. But, we can use a special builder to create testable services with all its collaborators replaced by fakes or test doubles. This way, we don’t need to create builder methods for every combination of services we need inside our tests.
Let me introduce you to TypeBuilder. This is a helper class I’ve been using in one of my client’s projects to create services inside our unit tests.
This TypeBuilder class uses reflection to find all the parameters in the constructor of the service to build. And, it uses Moq to build fakes for each parameter.
TypeBuilder expects a single constructor. But, we can easily extend it to pick the one with more parameters.
Let’s rewrite our sample test to use the TypeBuilder class.
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;namespaceWithTypeBuilder;[TestClass]publicclassOrderServiceTestsTypeBuilder{[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGateway(){// 1. Create a buildervartypeBuilder=newTypeBuilder<OrderService>();// ^^^^^// 2. Configure a IStockService fake with MoqtypeBuilder.WithMock<IStockService>(mock=>// ^^^^^{mock.Setup(t=>t.IsStockAvailable(It.IsAny<Order>())).Returns(true);});// 3. Build an OrderService instancevarservice=typeBuilder.Build();// ^^^^^varorder=newOrder();service.PlaceOrder(order);// Retrieve a fake from the buildertypeBuilder.Mock<IPaymentGateway>()// ^^^^.Verify(t=>t.ProcessPayment(It.IsAny<Order>()));}}
This is what happened. First, we create a builder with var typeBuilder = new TypeBuilder<OrderService>();.
Then, to register a custom fake, we used the method WithMock<T>(). And inside it, we configured the behavior of the fake.
In our case, we created a fake StockService that returns true for any order. We did that in these lines:
After that, with the method Build() we got an instance of the OrderService class with fakes for all its parameters. But, the fake for IStockService has the behavior we added in the previous step.
Finally, in the Assert part, we retrieved a fake from the builder with Mock<T>(). We used it to verify if the payment gateway was called or not. We did this here:
This TypeBuilder class comes in handy to avoid creating builders manually for every service in our unit tests.
Did you notice in our example that we didn’t have to write fakes for all collaborators? We only did it for the IStockService. The TypeBuilder took care of the other fakes.
3. Auto-mocking with AutoFixture
If you prefer a more battle-tested solution, let’s replace our TypeBuilder with AutoFixture.
What AutoFixture does
From its docs, AutoFixture“is a tool designed to make Test-Driven Development more productive and unit tests more refactoring-safe”.
AutoFixture creates test data for us. It helps us to simplify the Arrange parts of our tests.
To start using AutoFixture, let’s install its NuGet package AutoFixture.
For example, we can create orders inside our tests with:
AutoFixture will initialize all properties of an object to random values. Optionally, we can hardcode our own values if we want to.
AutoMoq
AutoFixture has integrations with mocking libraries like Moq to create services with all its parameters replaced by fakes. To use these integrations, let’s install the NuGet package AutoFixture.AutoMoq.
Let’s rewrite our sample test, this time to use AutoFixture with AutoMoq. It will look like this:
usingAutoFixture;usingAutoFixture.AutoMoq;usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;namespaceWithAutoFixture;[TestClass]publicclassOrderServiceTestsAutoFixture{// 1. Create a field for AutoFixtureprivatereadonlyIFixtureFixture=newFixture().Customize(newAutoMoqCustomization());// ^^^^^[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGateway(){varstockedService=Fixture.Freeze<Mock<IStockService>>();// ^^^^^// 2. Use Freeze to create a custom fakestockedService.Setup(t=>t.IsStockAvailable(It.IsAny<Order>())).Returns(true);varpaymentGateway=Fixture.Freeze<Mock<IPaymentGateway>>();varservice=Fixture.Create<OrderService>();// ^^^^^// 3. Use Create to grab an auto-mocked instancevarorder=newOrder();service.PlaceOrder(order);paymentGateway.Verify(t=>t.ProcessPayment(order));}}
Notice this time, we used a field in our test to hold a reference to AutoFixture Fixture class. Also, we needed to add the AutoMoqCustomization behavior to make AutoFixture a type builder.
To retrieve a fake reference, we used the Freeze<T>() method. We used these references to plug the custom behavior for the IStockService fake and to verify the IPaymentGateway fake.
Voilà! That’s how we can use a TypeBuilder helper class and AutoFixture to simplify the Arrange parts of our tests. If you prefer a simple solution, use the TypeBuilder class. But, if you don’t mind adding an external reference to your tests, use AutoFixture. Maybe, you can use it to create test data too.
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 covered what fakes are in unit testing and the types of fakes. We wrote two tests for an OrderService to show the difference between stubs and mocks.
In case you missed it, fakes are like test “simulators.” They replace external dependencies with testable components. Stubs and mocks are two types of fakes. Stubs are “simulators” that provide values or exceptions. And mocks are “simulators” that record method calls.
Before we start with the tips, let’s bring back the OrderService class from our last post.
In our last post, we chose certain names for our fakes like AlwaysAvailableStockService and FixedDateClock. Let’s see why those names and what to do and not to do when working with fakes.
TL;DR
Don’t assert on stubs
Keep one mock per test
Avoid logic inside your fakes
Make tests set their own values for fakes
Name your fakes properly
1. Don’t assert on stubs
Let’s remember that stubs are there to provide values indirectly to our code under test. We make fakes return a value or throw an exception.
Don’t write assertions for stubs. We don’t need them. Let’s assert on the result of our tests or use mocks.
Please, don’t do this.
[TestClass]publicclassOrderServiceTests{[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGateway(){varpaymentGateway=newFakePaymentGateway();varstockService=newAlwaysAvailableStockService();varservice=newOrderService(paymentGateway,stockService);varorder=newOrder();service.PlaceOrder(order);Assert.IsTrue(paymentGateway.WasCalled);// We don't need this assertionAssert.IsTrue(stockService.IsStockAvailable(order));// ^^^^^}}
If we write assertions for our stubs, we’re testing the mocking library, not our code.
2. Keep one mock per test
In the same spirit of keeping a single assertion per test, let’s keep one mock per test. Let’s have small and well-named tests.
Let’s say that in our OrderService, we need to log every request we made to charge a credit card and we add an ILogger<AccountService> to our service.
Please, don’t write tests with more than one mock. Like this one,
[TestClass]publicclassOrderServiceTests{[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGatewayAndLog(){varlogger=newFakeLogger();varpaymentGateway=newFakePaymentGateway();varstockService=newAlwaysAvailableStockService();varservice=newOrderService(logger,paymentGateway,stockService);varorder=newOrder();service.PlaceOrder(order);Assert.IsTrue(paymentGateway.WasCalled);Assert.IsTrue(logger.WasCalled);// ^^^^^// Let's keep one mock per test}}
Don’t use multiple mocks per test. Let’s write separate tests, instead.
For example, let’s not add flags to our stubs to return one value or another. Let’s write separate fakes, instead.
Let’s test the OrderService with and without stock. As a counterexample, let’s use a single FakeStockService with a flag to signal the two scenarios.
[TestClass]publicclassOrderServiceTests{[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGateway(){varpaymentGateway=newFakePaymentGateway();// Make the stock service have stockvarstockService=newFakeStockService{ItemInStock=true// ^^^^^};varservice=newOrderService(paymentGateway,stockService);varorder=newOrder();service.PlaceOrder(order);Assert.IsTrue(paymentGateway.WasCalled);}[TestMethod]publicvoidPlaceOrder_ItemOutOfStock_ThrowsException(){varpaymentGateway=newFakePaymentGateway();// Make the stock service have NO stockvarstockService=newFakeStockService{ItemInStock=false// ^^^^^};varservice=newOrderService(paymentGateway,stockService);varorder=newOrder();Assert.ThrowsException<OutOfStockException>(()=>service.PlaceOrder(order));}}
Here our fake service uses a bool, but it could start getting more complex if we need to support more test scenarios.
Let’s not use a single fake for both scenarios. Instead, let’s write two separate fakes and make each test use a different one. Let’s name these two fakes: ItemInStockStockService and ItemOutOfStockStockService. Inside them, we always return true and false, respectively.
[TestClass]publicclassOrderServiceTests{[TestMethod]publicvoidPlaceOrder_ItemInStock_CallsPaymentGateway(){varpaymentGateway=newFakePaymentGateway();// One fake for the "in stock" scenariovarstockService=newItemInStockStockService();// ^^^^^varservice=newOrderService(paymentGateway,stockService);varorder=newOrder();service.PlaceOrder(order);Assert.IsTrue(paymentGateway.WasCalled);}[TestMethod]publicvoidPlaceOrder_ItemOutOfStock_ThrowsException(){varpaymentGateway=newFakePaymentGateway();// Another fake for the "out of stock" scenariovarstockService=newItemOutOfStockStockService();// ^^^^^varservice=newOrderService(paymentGateway,stockService);varorder=newOrder();Assert.ThrowsException<OutOfStockException>(()=>service.PlaceOrder(order));}}
Don’t worry about creating lots of fakes. Fakes are cheap. Any decent IDE can create a class implementing an interface or an abstract class with a few clicks or a single keyboard shortcut.
4. Make tests set their own values for fakes
Avoid magic values in your stubs. Let’s make tests pass their own values instead of having hard-coded values in stubs.
Let’s say that StockService returns the units available instead of a simple true or false. Check this test,
It makes more sense! There’s only 1 unit available and we’re placing an order for 2 items. Let’s make tests fake their own values.
5. Name your fakes properly
Again for our last tip, let’s talk about names. Naming is hard!
Name stubs to indicate the value they return or the exception they throw.
We named our fake stock provider AlwaysAvailableStockService to show it always returns stock available. It obvious from its name what is the return value.
When we needed two stock providers to test the OrderService without stock, we named our fakes: ItemInStockStockService and ItemOutOfStockStockService.
Also, do you remember why we named our fake FixedDateClock? No? You can tell it by its name. It returns a fixed date, the DateTime you pass to it.
Voilà! Those are five tips to write better stubs and mocks. Remember, write dumb fakes. Don’t put too much logic in them. Let the tests fake their own values.
Do you know what are fakes? Are stubs and mocks the same thing? Do you know if you need any of them? Once I made the exact same questions. Let’s see what are fakes in unit testing.
In unit testing, fakes or test doubles are classes or components that replace external dependencies. Fakes simulate successful or failed scenarios to test the logic around the real dependencies they replace.
The best analogy to understand fakes are flight simulators. With a flight simulator, teachers create flight and environment conditions to train and test their pilot students in controlled scenarios.
Fakes are like flight simulators. Fakes return values, throw exceptions or record method calls to test the code around it. They create the conditions to test our code in controlled scenarios.
Let’s move to an example. In our last post when we wrote tests that use DateTime.Now, we slightly covered the concept of fakes. In that post, we wrote a validator for credit cards. It looks something like this:
publicclassCreditCardValidator:AbstractValidator<CreditCard>{publicCreditCardValidator(ISystemClocksystemClock){// Rest of code here...varnow=systemClock.Now;}}publicinterfaceISystemClock{System.DateTimeNow{get;}}publicclassSystemClock:ISystemClock{publicDateTimeNow=>DateTime.Now;}
We created a ISystemClock interface with a Now property to replace DateTime.Now inside our validator. Then, in the unit tests, instead of using SystemClock with the real date and time, we wrote a FixedDateClock class to always return the same date and time.
This is one of the tests where wrote that uses the FixedDateClock class.
Well, that FixedDateClock is a fake. It replaces the SystemClock holding the real date and time with a testable alternative. With that fake in place, we make our tests use any date and time we want instead of the real date and time.
To be more precise, the FixedDateClock is a stub. But, let’s find out about stubs and mocks.
What’s the difference between Mocks and Stubs?
Now that we know what fakes are, let’s see two types of fakes: mocks and stubs. This is the difference between them.
Both mocks and stubs are fakes or test doubles. Stubs provide values or exceptions to the code under test and mocks are used to assert that a method was called with the right parameters.
OrderService example
To better understand the difference between mocks and stubs, let’s use another example. Let’s process online orders with an OrderService class.
This OrderService checks if an item has stock available to then charge a credit card. Imagine it uses an online payment processing software and a microservice to find the stock of an item. We use two interfaces, IPaymentGateway and IStockService, to represent the two dependencies. Something like this,
The AlwaysAvailableStockService fake is there to provide a value for our test. It’s a stub. And, the FakePaymentGateway is used to assert that the OrderService called the method to charge a credit card. It’s a mock. Actually, we could call it MockPaymentGateway.
Again, stubs provides values and mocks are used to assert.
Let’s use fakes in our unit tests when we depend on external systems we don’t control. For example, third-party APIs and message queues. Let’s assert the right call were made or the right messages were sent.
Other types of fakes: dummies, stubs, spies and mocks
We learned about mocks and stubs. But, there are more types of fakes or doubles.
The book xUnit Patterns presents a broader category of fakes. It uses: dummies, stubs, spies and mocks. Let’s quickly go through them.
Dummies are used to respect the signature of methods and classes under test. A dummy is never called inside the code under test. A null value or a null object, like NullLogger, in a class constructor are dummies when we’re testing one of the class methods that doesn’t use that parameter.
Stubs feed our code under test with indirect input values. We use stubs when the real dependencies aren’t available in the test environment or when using one will have side effects. Like charging a credit card. For “xUnit Patterns” stubs are exactly the same as what we described earlier.
Spies are observation points added to the code under test. We use spies to check if the code under test called another component or not, and the parameters it used. According to “xUnit Patterns”, mocks we wrote earlier are actually spies.
Mocks are testable replacements that check if they were used correctly. We use mocks when we know in advanced the parameters the code under test will use. With mocks, we set the expected parameters to be used before calling the code under test. Then, we use a verification method in the mock itself to check if the mock was called with those exact same parameters.
Let’s not get confused with all these terms. Let’s stick to the types of fakes presented in the book The Art of Unit Testing. In there, there are only two types of fakes or test doubles: stubs and mocks. Everything else is a fake. Easier!
Parting thoughts
Voilà! That’s what fakes are in unit testing. Remember, stubs provide values for our tests and mocks assert that calls were made. That’s the difference between them.
Often, we use the terms fake, stubs and mocks interchangeably. And sometimes we use the term “mocking” to mean the replacement of external components with testable equivalents. But, we have seen there’s a distinction between all these terms.
And don’t miss the rest of my Unit Testing 101 series where I cover more subjects like this one.
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.
In our last post about using builders to create test data, we wrote a validator for expired credit cards. We used DateTime.Now all over the place. Let’s see how to write better unit tests that use the current time.
To write tests that use DateTime.Now, create a wrapper for DateTime.Now and use a fake or test double with a fixed date. As an alternative, create a setter or an optional constructor to pass a reference date.
Let’s continue where we left off. Last time, we wrote two tests to check if a credit card was expired using the Builder pattern. These are the tests we wrote at that time.
These two tests rely on the current date and time. Every time we run tests that rely on the current date and time, we will have a different date and time. It means we will have different test values and tests each time we run these tests.
We want our tests to be deterministic. We learned that from Unit Testing 101. Using DateTime.Now in our tests isn’t a good idea.
What are seams?
To replace the DateTime.Now in our tests, we need seams.
A seam is a place to introduce testable behavior in our code under test. Two techniques to introduce seams are interfaces to declare dependencies in the constructor of a service and optional setter methods to plug in testable values.
Let’s see these two techniques to replace the DateTime.Now in our tests.
1. Use a fake or test double to replace DateTime.Now
To make our tests more reliable, let’s create an abstraction for the current time and make our validator depend on it. Later, we can pass a fake or test double with a hardcoded date in our tests.
Let’s create an ISystemClock interface and a default implementation. The ISystemClock will have a Now property for the current date and time.
Our CreditCardValidator will receive in its constructor a reference to ISystemClock. Then, instead of using DateTime.Now in our validator, it will use the Now property from the clock.
publicclassCreditCardValidator:AbstractValidator<CreditCard>{publicCreditCardValidator(ISystemClocksystemClock){varnow=systemClock.Now;// Beep, beep, boop// Rest of the code here...}}
Next, let’s create a testable clock and use it in our tests.
Notice we named our fake clock FixedDateClock to show it returns the DateTime we pass to it.
Our tests with the testable clock implementation will look like this,
[TestClass]publicclassCreditCardValidationTests{[TestMethod]publicvoidCreditCard_ExpiredYear_ReturnsInvalid(){varwhen=newDateTime(2021,01,01);varclock=newFixedDateClock(when);// This time we're passing a fake clock implementationvarvalidator=newCreditCardValidator(clock);// ^^^^^varrequest=newCreditCardBuilder().WithExpirationYear(when.AddYears(-1).Year)// ^^^^.Build();varresult=validator.TestValidate(request);result.ShouldHaveAnyValidationError();}[TestMethod]publicvoidCreditCard_ExpiredMonth_ReturnsInvalid(){varwhen=newDateTime(2021,01,01);varclock=newFixedDateClock(when);varvalidator=newCreditCardValidator(clock);// ^^^^^varrequest=newCreditCardBuilder().WithExpirationMonth(when.AddMonths(-1).Month)// ^^^^.Build();varresult=validator.TestValidate(request);result.ShouldHaveAnyValidationError();}}
With a testable clock in our tests, we replaced all the references to DateTime.Now with a fixed date in the past.
UPDATE (June 2024): .NET 8.0 introduced TimeProvider, a new abstraction to use and test time. With this new built-in abstraction, we don’t need to roll our own ISystemClock.
Create constants for common test values
To make things cleaner, let’s refactor our tests. Let’s use a builder method and read-only fields for the fixed dates.
That’s how we can abstract the current date and time with an interface.
2. Use a parameter in a constructor
Now, let’s see the second alternative. To replace the interface from our first example, in the constructor we can pass a delegate returning a reference date. Like this:
publicclassCreditCardValidator:AbstractValidator<CreditCard>{publicCreditCardValidator(Func<DateTime>nowSelector)// ^^^^^{varnow=nowSelector();// Beep, beep, boop// Rest of the code here...}}
Or, even simpler we can pass a plain DateTime parameter. Like this:
publicclassCreditCardValidator:AbstractValidator<CreditCard>{publicCreditCardValidator(DateTimenow)// ^^^^^{// Beep, beep, boop// Rest of the code here...}}
Let’s stick to a simple parameter and update our tests.
Another variation on this theme is to create a setter inside the CreditCardValidator to pass an optional date. Inside the validator, we should check if the optional date is present to use DateTime.Now or not. Something like this,
And don’t miss the rest of my Unit Testing 101 series where I cover more subjects like this one.
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.