These days I had to review some code that expected a controller to log the exceptions thrown in a service. This is how that controller looked and what I learned about testing logging messages.
When writing unit tests for logging, assert that actual log messages contain keywords like identifiers or requested values. Don’t assert that actual and expected log messages are exactly the same.
Don’t expect identical log messages
The controller I reviewed looked like this,
usingMicrosoft.AspNetCore.Mvc;usingOnTestingLogMessages.Services;namespaceOnTestingLogMessages.Controllers;[ApiController][Route("[controller]")]publicclassSomethingController:ControllerBase{privatereadonlyIClientService_clientService;privatereadonlyILogger<SomethingController>_logger;publicSomethingController(IClientServiceclientService,ILogger<SomethingController>logger){_clientService=clientService;_logger=logger;}[HttpPost]publicasyncTask<IActionResult>PostAsync(AnyPostRequestrequest){try{// Imagine that this service does something interesting...await_clientService.DoSomethingAsync(request.ClientId);returnOk();}catch(Exceptionexception){_logger.LogError(exception,"Something horribly wrong happened. ClientId: [{clientId}]",request.ClientId);// ^^^^^^^^// Logging things like good citizens of the world...returnBadRequest();}}// Other methods here...}
Nothing fancy. It called an IClientService service and logged the exception thrown by it. Let’s imagine that the controller logged a more helpful message to troubleshoot later. I wrote a funny log message here. Yes, exception filters are a better idea, but bear with me.
To test if the controller logs exceptions, we could write a unit test like this,
usingMicrosoft.Extensions.Logging;usingMoq;usingOnTestingLogMessages.Controllers;usingOnTestingLogMessages.Services;namespaceOnTestingLogMessages.Tests;[TestClass]publicclassSomethingControllerTests{[TestMethod]publicasyncTaskPostAsync_Exception_LogsException(){varclientId=123456;varfakeClientService=newMock<IClientService>();fakeClientService.Setup(t=>t.DoSomethingAsync(clientId)).ThrowsAsync(newException("Expected exception..."));// ^^^^^// 3...2...1...Boom...varfakeLogger=newMock<ILogger<SomethingController>>();varcontroller=newSomethingController(fakeClientService.Object,fakeLogger.Object);// ^^^^^varrequest=newAnyPostRequest(clientId);awaitcontroller.PostAsync(request);varexpected=$"Something horribly wrong happened. ClientId: [{clientId}]";// ^^^^^^^^// We expect exactly the same log message from the PostAsyncfakeLogger.VerifyWasCalled(LogLevel.Error,expected);}}
By the way, .NET 8.0 added a FakeLogger, a logging provider for unit testing, so we don’t have to rely on fakes to test logging.
In our test, we’re expecting the actual log message to be exactly the same as the one from the SomethingController. Can you already spot the duplication? In fact, we’re rebuilding the log message inside our tests. We’re duplicating the logic under test.
Also, let’s notice we used a custom assertion method to make our assertions less verbose. VerifyWasCalled() is an extension method that inspects the Moq instance to check if the actual and expected messages are equal. Here it is,
To make our unit tests more maintainable, let’s check that log messages contain keywords or relevant substrings, like identifiers and values from input requests. Let’s not check if they’re identical to the expected log messages. Any changes in casing, punctuation, spelling or any other minor changes in the message structure will make our tests break.
Let’s rewrite our test,
usingMicrosoft.Extensions.Logging;usingMoq;usingOnTestingLogMessages.Controllers;usingOnTestingLogMessages.Services;namespaceOnTestingLogMessages.Tests;[TestClass]publicclassSomethingControllerTests{[TestMethod]publicasyncTaskPostAsync_Exception_LogsException(){varclientId=123456;varfakeClientService=newMock<IClientService>();fakeClientService.Setup(t=>t.DoSomethingAsync(clientId)).ThrowsAsync(newException("Expected exception..."));// ^^^^^// 3...2...1...Boom...varfakeLogger=newMock<ILogger<SomethingController>>();varcontroller=newSomethingController(fakeClientService.Object,fakeLogger.Object);varrequest=newAnyPostRequest(clientId);awaitcontroller.PostAsync(request);fakeLogger.VerifyMessageContains(LogLevel.Error,clientId.ToString());// ^^^^^^^^// We expect the same log message to only contain the clientId}}
This time, we rolled another extension method, VerifyMessageContains(), removed the expected log message and asserted that the log message only contained only relevant subtrings: the clientId.
Here it is the new VerifyMessageContains(),
publicstaticclassMoqExtensions{publicstaticvoidVerifyMessageContains<T>(thisMock<ILogger<T>>fakeLogger,LogLevellogLevel,paramsstring[]expected){fakeLogger.Verify(x=>x.Log(logLevel,It.IsAny<EventId>(),It.Is<It.IsAnyType>((o,t)=>expected.All(s=>o.ToString().Contains(s,StringComparison.OrdinalIgnoreCase))),// ^^^^^// Checking if the log message contains some keywords, insteadIt.IsAny<Exception>(),It.IsAny<Func<It.IsAnyType,Exception?,string>>()),Times.Once);}}
Voilà! That’s how to make our test that checks logging messages more maintainable. By not rebuilding log messages inside tests and asserting that they contain keywords instead of expecting to be exact matches.
Here we dealt with logging for diagnostic purposes (logging to make troubleshooting easier for developers). But if logging were a business requirement, we should have to make it a separate “concept” in our code. Not in logging statements scatter all over the place. I learned by distinction about logging when reading Unit Testing Principles, Practices, and Patterns.
Ready to upgrade your unit testing skills? Write readable and maintainable unit test with my course Mastering C# Unit Testing with Real-world Examples on Udemy. Learn unit testing best practices while refactoring real unit tests from my past projects. No tests for a Calculator class.
These days I needed to work with a microservice for one of my clients. In that microservice, instead of validating incoming requests with the built-in model validations or FluentValidation, they use authorization filters. I needed to write some tests for that filter. This is what I learned.
Apart from validating the integrity of the incoming requests, the filter also validated that the referenced object in the request body matched the same “client.”
A weird filter scenario
The filter looked something like this,
usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.AspNetCore.Mvc.Controllers;usingMicrosoft.AspNetCore.Mvc.Filters;usingNewtonsoft.Json;usingMyWeirdFilterScenario.Controllers;namespaceMyWeirdFilterScenario.Filters;publicclassMyAuthorizationFilter:IAsyncAuthorizationFilter{privatereadonlyDictionary<string,Func<AuthorizationFilterContext,Task<bool>>>_validationsPerEndpoint;privatereadonlyIClientRepository_clientRepository;privatereadonlyIOtherEntityRepository_otherEntityRepository;publicMyAuthorizationFilter(IClientRepositoryclientRepository,IOtherEntityRepositoryotherEntityRepository){_clientRepository=clientRepository;_otherEntityRepository=otherEntityRepository;// Register validations per action name here// vvvvv_validationsPerEndpoint=newDictionary<string,Func<AuthorizationFilterContext,Task<bool>>>(StringComparer.OrdinalIgnoreCase){{nameof(SomethingController.Post),ValidatePostAsync},// Register validations for other methods here...};}publicasyncTaskOnAuthorizationAsync(AuthorizationFilterContextcontext){varactionName=((ControllerActionDescriptor)context.ActionDescriptor).ActionName;try{varvalidation=_validationsPerEndpoint[actionName];varisValid=awaitvalidation(context);// ^^^^^^^^^^// Grab and run the validation for the called endpointif(!isValid){context.Result=newBadRequestResult();return;}}catch(Exception){// Log bad things here...context.Result=newBadRequestResult();}}privateasyncTask<bool>ValidatePostAsync(AuthorizationFilterContextcontext){varrequest=awaitGetRequestBodyAsync<AnyPostRequest>(context);// ^^^^^^^^^^^^^^^^^^^// Grab the request bodyif(request==null||request.ClientId==default){returnfalse;}varclient=await_clientRepository.GetByIdAsync(request.ClientId);// ^^^^^^// Check our client exists...if(client==null){returnfalse;}varotherEntity=await_otherEntityRepository.GetByIdAsync(request.OtherEntityId);if(otherEntity==null||otherEntity.ClientId!=client.Id)// ^^^^^^^^^^^// Check we're updating our own entity...{returnfalse;}// Doing something else here...returntrue;}// A helper method to grab the request body from the AuthorizationFilterContextprivatestaticasyncTask<T?>GetRequestBodyAsync<T>(AuthorizationFilterContextcontext){varrequest=context.HttpContext.Request;request.EnableBuffering();request.Body.Position=0;varbody=newStreamReader(request.Body);varrequestBodyJson=awaitbody.ReadToEndAsync();request.Body.Position=0;if(string.IsNullOrEmpty(requestBodyJson)){returndefault;}varsettings=newJsonSerializerSettings{NullValueHandling=NullValueHandling.Ignore};varrequestBody=JsonConvert.DeserializeObject<T>(requestBodyJson,settings);returnrequestBody;}}
On the OnAuthorizationAsync() method, this filter grabbed the validation method based on the called method name. And, inside the validation method, it checked that the request had a valid “clientId” and the referenced entity belonged to the same client. This is to prevent any client from updating somebody else’s entities.
Also, notice we needed to use the EnableBuffering() and reset the body’s position before and after reading the body from the AuthorizationFilterContext.
On the controller side, we registered the filter with an attribute like this,
usingMicrosoft.AspNetCore.Mvc;usingRecreatingFilterScenario.Filters;namespaceMyAuthorizationFilter.Controllers;[ApiController][Route("[controller]")][ServiceFilter(typeof(MyAuthorizationFilter))]// ^^^^^^^^^^^publicclassSomethingController:ControllerBase{[HttpPost]publicvoidPost(AnyPostRequestrequest){// Beep, beep, boop...// Doing something with request}// Other methods here...}
And, to make it work, we also need to register our filter in the dependencies container.
How to test an ASP.NET async authorization filter
To test an ASP.NET async filter, create a new instance of the filter passing the needed dependencies as stubs. Then, when calling the OnAuthorizationAsync() method, create a AuthorizationFilterContext instance attaching the request body inside a DefaultHttpContext.
Like this,
usingMicrosoft.AspNetCore.Http;usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.AspNetCore.Mvc.Controllers;usingMicrosoft.AspNetCore.Mvc.Filters;usingMicrosoft.AspNetCore.Routing;usingMoq;usingNewtonsoft.Json;usingRecreatingFilterScenario.Controllers;usingRecreatingFilterScenario.Filters;usingSystem.Text;namespaceMyWeirdFilterScenario.Tests;[TestClass]publicclassMyAuthorizationFilterTests{[TestMethod]publicasyncTaskOnAuthorizationAsync_OtherEntityWithoutTheSameClient_ReturnsBadRequest(){varsameClientId=1;varotherClientId=2;varotherEntityId=123456;varfakeClientRepository=newMock<IClientRepository>();fakeClientRepository.Setup(t=>t.GetByIdAsync(sameClientId)).ReturnsAsync(newClient(sameClientId));varfakeOtherEntityRepository=newMock<IOtherEntityRepository>();fakeOtherEntityRepository.Setup(t=>t.GetByIdAsync(otherEntityId)).ReturnsAsync(newOtherEntity(otherClientId));varfilter=newMyAuthorizationFilter(fakeClientRepository.Object,fakeOtherEntityRepository.Object);// ^^^^^^// Create an instance of our filter with two fake dependenciesvarrequest=newAnyPostRequest(sameClientId,otherEntityId);varcontext=BuildContext(request);// ^^^^^^^^^^^^// Create an AuthorizationFilterContextawaitfilter.OnAuthorizationAsync(context);Assert.IsNotNull(context.Result);Assert.AreEqual(typeof(BadRequestResult),context.Result.GetType());}privateAuthorizationFilterContextBuildContext(AnyPostRequest?request){varhttpContext=newDefaultHttpContext();varjson=JsonConvert.SerializeObject(request);varstream=newMemoryStream(Encoding.UTF8.GetBytes(json));httpContext.Request.Body=stream;httpContext.Request.ContentLength=stream.Length;httpContext.Request.ContentType="application/json";// ^^^^^^^^// Attach a JSON bodyvaractionDescriptor=newControllerActionDescriptor{ActionName=nameof(SomethingController.Post)// ^^^^^^^// Use the endpoint name};varactionContext=newActionContext(httpContext,newRouteData(),actionDescriptor);returnnewAuthorizationFilterContext(actionContext,newList<IFilterMetadata>());}}
Let’s unwrap it. First, we created an instance of MyAuthorizationFilter passing the dependencies as fakes using Moq. As stubs, to be precise.
To call the OnAuthorizationAsync() method, we needed to create an AuthorizationFilterContext. This context required an ActionContext. We used a Builder method, BuildContext(), to keep things clean.
Then, to create an ActionContext, we needed to attach the request body as JSON to a DefaultHttpContext and set the action descriptor with our method name. Since we didn’t read any route information, we passed a default RouteData instance.
Notice that we needed to use a MemoryStream to pass our request object as JSON and set the content length and type. Source.
With the BuildContext() method in place, we got the Arrange and Act parts of our sample test. The next step was to assert on the context result.
Voilà! That’s what I learned about unit testing ASP.NET authorization filters. Again, a Builder method helped to keep things simple and easier to reuse.
Ready to upgrade your unit testing skills? Write readable and maintainable unit test with my course Mastering C# Unit Testing with Real-world Examples on Udemy. Learn unit testing best practices while refactoring real unit tests from my past projects. No tests for a Calculator class.
This is a lesson I learned after trying to use a shared NuGet package in one of my client’s projects and getting an ArgumentNullException. I had no clue that I needed some configuration values in my appsettings.json file. This is what I learned.
Always check for missing configuration values inside constructors. In case they’re not set, throw a human-friendly exception message showing the name of the expected configuration value. For example: ‘Missing Section:Subsection:Value in config file’.
A missing configuration value
This is what happened. I needed to import a feature from a shared Nuget package. It had a method to register its dependencies. Something like services.AddFeature().
When calling an API endpoint that used that feature, I got an ArgumentNullException: “Value cannot be null. (Parameter ‘uriString’).” It seemed that I was missing a URI. But what URI?
Without any XML docstrings on the AddFeature() method, I had no other solution than to decompile that DLL. I found a service like this one,
publicclassSomeService:ISomeService{privatereadonlyUri_anyUri;publicSomeService(IOptions<AnyConfigOptions>options,OtherParamotherParam){_anyUri=newUri(options.Value.AnyConfigValue);// ^^^^^^^// System.ArgumentNullException: Value cannot be null. (Parameter 'uriString')}publicasyncTaskDoSomethingAsync(){// Beep, beep, boop...// Doing something here...}}
Then I realized that a validation inside the constructor with a human-friendly message would have saved me (and any other future developer using that NuGet package) some time. And it would have pointed me in the right direction. I mean having something like,
publicclassSomeService:ISomeService{privatereadonlyUri_anyUri;publicSomeService(IOptions<AnyConfigOptions>options,OtherParamotherParam){// vvvvvvvif(string.IsNullOrEmpty(options?.Value?.AnyConfigValue)){thrownewArgumentNullException("Missing 'AnyConfigOptions:AnyConfigValue' in config file.");// ^^^^^^^^// I think this would be a better message}_anyUri=newUri(options.Value.AnyConfigValue);}publicasyncTaskDoSomethingAsync(){// Beep, beep, boop...// Doing something here again...}}
Even better, what if the AddFeature() method had an overload that receives the expected configuration value? Something like AddFeature(AnyConfigOptions options). This way, the client of that package could decide the source of those options. Either read them from a configuration file or hardcode them.
The book “Growing Object-Oriented Software Guided by Tests” suggests having a StupidProgrammerMistakeException or a specific exception for this type of scenario: missing configuration values. This would be a good use case for that exception type.
Voilà! That’s what I learned today: always validate configuration values inside constructors and use explicit error messages when implementing the Options pattern. It reminded me of “The given key was not present in the dictionary” and other obscure error messages. Do you write friendly and clear error messages?
These days I needed to unit test a service that used the built-in HttpClient. It wasn’t as easy as creating a fake for HttpClient. This is how to write tests for HttpClient with Moq and a set of extension methods to make it easier.
To write tests for a service that requires a HttpClient, create a fake for HttpMessageHandler and set up the protected SendAsync() method to return a HttpResponseMessage. Then, create a new HttpClient passing the fake instance of HttpMessageHandler created before.
How to Create a Testable HttpClient
For example, let’s write a test for a AnyService class that receives a HttpClient, using MSTest and Moq,
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;usingMoq.Protected;usingNewtonsoft.Json;usingSystem.Net;usingSystem.Net.Http;usingSystem.Threading;usingSystem.Threading.Tasks;namespaceMyProject.Services.Tests;[TestClass]publicclassAnyServiceTests{[TestMethod]publicasyncTaskDoSomethingAsync_ByDefault_ReturnsSomethingElse(){varfakeHttpMessageHandler=newMock<HttpMessageHandler>();fakeHttpMessageHandler.Protected()// ^^^^^^^.Setup<Task<HttpResponseMessage>>("SendAsync",ItExpr.IsAny<HttpRequestMessage>(),ItExpr.IsAny<CancellationToken>()).ReturnsAsync(newHttpResponseMessage{StatusCode=HttpStatusCode.OK,Content=newStringContent(JsonConvert.SerializeObject(newAnyResponseViewModel()))// We add the expected response here: ^^^^^});usingvarhttpClient=newHttpClient(fakeHttpMessageHandler.Object);// ^^^^^varservice=newAnyService(client);varsomeResult=awaitservice.DoSomethingAsync();// Assert something here...Assert.IsNotNull(someResult);}}
Notice how we used the Protected() and Setup() methods from Moq to create a fake for HtttpMessageHandler. Then, inside the ReturnsAsync() method, we created a response message with a response object. And, finally, we used the fake handler to create a new HttpClient to pass it to our AnyService instance.
That’s how we created a fake HttpClient. But, as soon as we start to write more tests, all of them get bloated with lots of duplicated code. Especially, if we create new tests by copy-pasting an existing one.
We should reduce the noise in our tests using factory methods or builders to make our tests more readable. Let’s do that!
Some extensions methods to set up the faked HttpClient
It would be great if we could reduce the Arrange phase of our sample test to one or two lines. Something like this,
It’s not that difficult to write some extension methods on top of the Mock<HttpMessageHandler> to simplify the creation of testable HttpClient instances.
We can add other methods like WithNotFoundResponse(), WithInternalServerResponse() or WithTooManyRequestsResponse() to cover other response codes. Even, we can setup the fake HttpMessageHandler passing an Uri with a method ForUri(), for example.
Voilà! That’s how to write tests with HttpClient and Moq. With some extension methods, we could have a small DSL to write more readable tests. For a more fully-featured alternative to write tests for HttpClient, check mockhttp, “a testing layer for Microsoft’s HttpClient library.”
If you want to read more about unit testing, check my Unit Testing 101 series where we cover from what a unit test is, to fakes and mocks, to best practices.
This year, inspired by C# Advent and 24 Pull Requests, I decided to do my own Christmas challenge: my own Advent of Code. I prefer to call it: Advent of Posts. Starting on December 1st, I’m publishing 24 posts, one post per day.
The challenge is to write an article per day in about 2 hours, including proof-reading and banner design. I’ve written some of the post in advance to avoid content pressure.