Unit testing cheat sheet (xUnit, FakeItEasy, FluentAssertions)
Here are some tips, samples, and suggestions for implementing common unit testing tasks.
Unit test without parameters:
[Fact] void SampleClassName_MethodToBeTested_DescriptiveTestTitle() { // ARRANGE // ACT // ASSERT }
Unit test with parameters:
[Theory] [InlineData("abc", 1, true)] [InlineData("xyz", 2, false)] void SampleClassName_MethodToBeTested_DescriptiveTestTitle ( string? param1, int? param2, bool? param3 ) { // ARRANGE // ACT // ASSERT }
Unit tests with DateTime or DateTimeOffset parameters:
[Theory] [InlineData("2024-06-05 23:45:10.456", "3/28/2007 12:13:50 PM -07:00")] void SampleClassName_MethodToBeTested_DescriptiveTestTitle ( string? paramDateTime, string? paramDateTimeOffset ) { DateTime dateTime = DateTime.Parse(paramDateTime); DateTime dateTimeOffset = DateTimeOffset.Parse(paramDateTimeOffset); ... }
Unit tests with complex parameters:
[Theory] [InlineData("{\"id\":123,\"email\":\"joe.doe@somemail.com\",\"enabled\":true}")] void SampleClassName_MethodToBeTested_DescriptiveTestTitle ( string? paramUser ) { // This example uses Newtonsoft.Json, but the same can be done // using the default framework's JSON serializer. User? user = JsonConvert.DeserializeObject<User?>(paramUser); // NOTE: Yes, I know about MemberData, but this seems more straightforward IMHO. ... }
A fake object with the default constructor (can use an interface or a class):
ISample fakeISample = A.Fake<ISample>(); Sample fakeSample = A.Fake<Sample>(); ISample<AnotherSample> fakeGenericSample = A.Fake<ISample<AnotherSample>>();
A fake object with a parametrized constructor:
Sample fakeSample = A.Fake<Sample>(x => x.WithArgumentsForConstructor(new object[] { "param1", 2, true }));
A fake object returns specific property values:
User user = A.Fake<User>(); A.CallTo(() => user.Id).Returns(12345); A.CallTo(() => user.Email).Returns("joe.doe@somemail.com"); A.CallTo(() => user.Enabled).Returns(true);
A fake object returns a specific method result:
// Data service is used by user service to get user from database // and we are faking it. IDataService dataService = A.Fake<IDataService>(); // Define properties of the user object to be retuned. int id = 12345; // Let's assume that the method of the UserService class being tested // internally calls the GetUserById method of the IDataService object // (we're using a fake here to simulate a valid return). A.CallTo(() => dataService.GetUserById(id)).Returns(new User(id)); // UserService is the class we're testing (system under test or SUT). UserService userService = new UserService(dataService); // We are testing the Enable method and expect it to be successful. userService.Enable(id);
Use wildcard to trigger a fake method result for any parameter value:
// A<T>._ is a shortcut for a wildcard. A.CallTo(() => dataService.GetUserById(A<string>._)).Returns(existingUser);
A fake object returns a specific value from an async method:
IDataService dataService = A.Fake<IDataService>(); // Define properties of the user object to be retuned. int id = 12345; // Assume that GetUserById is an async method returning Task<User>. A.CallTo(() => dataService.GetUserById(A<string>._)).Returns(Task.FromResult(new User(id)));
A fake object returns a specific value from a generic method:
// Data service is used by user service to get user from database // and we are faking it. IDataService dataService = A.Fake<IDataService>(); // Define properties of the user object to be retuned. int id = 12345; // Data service has a generic method GetUser that we want to fake. A.CallTo(dataService).Where(call => call.Method.Name == "GetUser") .WithReturnType<User>() .Returns(new User(id)); // UserService is the class we're testing (system under test or SUT). UserService userService = new UserService(dataService);
Force a fake method to throw an exception:
A.CallTo(() => dataService.GetUserById(A<string>._)).Throws<Exception>(); // Equivalent to: A.CallTo(() => dataService.GetUserById(A<string>._)).Throws(new Exception());
Expect a method to throw an exception of the exact type:
Assert.Throws<InvalidInputException>(() => userService.UpdateUser(user));
Expect a method to throw any exception derived from the specific type:
Assert.ThrowsAny<InvalidInputException>(() => userService.UpdateUser(user));
Assign expected exception to a variable:
Exception ex = Assert.Throws<InvalidInputException>(() => userService.UpdateUser(user));
Set up a fake SendGrid call:
ISendGridClient _sendGridClient = A.Fake<ISendGridClient>(); System.Net.Http.HttpResponseMessage httpResponse = new(); System.Net.Http.HttpContent httpContent = httpResponse.Content; System.Net.Http.HttpResponseHeaders httpHeaders = httpResponse.Headers; httpHeaders.Add("X-Message-Id", "12345"); SendGrid.Response sendGridResponse = A.Fake<SendGrid.Response>(x => x .WithArgumentsForConstructor(new object[] { httpStatusCode, httpContent, httpHeaders })); A.CallTo(() =< _sendGridClient .SendEmailAsync(A<SendGridMessage>._, A<CancellationToken>._)) .Returns(Task.FromResult(sendGridResponse));
Mock HttpContext for a controller class under test:
System.Net.Http.HttpRequest httpRequest = A.Fake<HttpRequest>(); System.Net.Http.HttpContext httpContext= A.Fake<HttpContext>(); A.CallTo(() => httpContext.Request).Returns(httpRequest); // Set up the request properies that you need. A.CallTo(() => httpRequest.Scheme).Returns("https"); A.CallTo(() => httpRequest.Host).Returns(new HostString("localhost:8888")); A.CallTo(() => httpRequest.PathBase).Returns(new PathString("/api/v1")); A.CallTo(() => httpRequest.Path).Returns(new PathString("sample")); // SampleController is derived from the ControllerBase class. SampleController controller = new(...); controller.ControllerContext = new ControllerContext() { HttpContext = httpContext };
Controller method GET returns HTTP status code 200 OK:
// Assume that all dependencies have been set. ActionResult<User> actionResult = controller.GetUser("1234567890"); // First, test action result. actionResult.Should().NotBeNull(); actionResult.Result.Should().NotBeNull(); actionResult.Result.Should().BeAssignableTo<OkObjectResult>(); // Next, test response specific result. OkObjectResult? result = actionResult.Result as OkObjectResult;
Controller method POST returns HTTP status code 201 Created:
// Assume that all dependencies have been set. ActionResult<User> actionResult = controller.CreateUser(user); // First, test action result. actionResult.Should().NotBeNull(); actionResult.Result.Should().NotBeNull(); actionResult.Result.Should().BeAssignableTo<CreatedResult>(); // Next, test response specific result. CreatedResult? result = actionResult.Result as CreatedResult; result.Should().NotBeNull(); // Successful POST must return the URL of the GET method // ending with the ID of the newly created object in the // Location header. result?.Location.Should().NotBeNull(); result?.Location.Should().EndWith(id);
Controller method PATCH returns HTTP status code 204 No Content:
// Assume that all dependencies have been set. ActionResult actionResult = controller.UpdateUser(user); // First, test action result. actionResult.Should().NotBeNull(); actionResult.BeAssignableTo<NoContentResult>();
Controller method POST returns HTTP status code 400 Bad Request:
// Assume that all dependencies have been set. ActionResult<User> actionResult = controller.CreateUser(user); // First, test action result. actionResult.Should().NotBeNull(); actionResult.Result.Should().NotBeNull(); actionResult.Result.Should().BeAssignableTo<BadRequestObjectResult>(); // Next, test response specific result. BadRequestObjectResult? result = actionResult.Result as BadRequestObjectResult; result.Should().NotBeNull(); // Finally, check the error object to be returned to consumer. // This example shows a custom problem details object ErroDetails, // which may be different in your case. result?.Value.Should().NotBeNull(); result?.Value.Should().BeAssignableTo<ErrorDetails>(); if (result?.Value is ErrorDetails errorDetails) { // ServiceCodeType is a custom enum value returned via the error object's // ServiceCode property (this check may be different in your case). errorDetails.ServiceCode.Should().NotBeNull(); errorDetails.ServiceCode.Should().Be(ServiceCodeType.BadRequest.ToString()); }
Controller method POST returns HTTP status code 401 Unauthorized:
// Assume that all dependencies have been set. ActionResult<User> actionResult = controller.CreateUser(user); // First, test action result. actionResult.Should().NotBeNull(); actionResult.Result.Should().NotBeNull(); actionResult.Result.Should().BeAssignableTo<UnauthorizedObjectResult>(); // Next, test response specific result. UnauthorizedObjectResult? result = actionResult.Result as UnauthorizedObjectResult; result.Should().NotBeNull(); // See example handling 400 Bad Request.
Controller method PATCH returns HTTP status code 404 Not Found:
// Assume that all dependencies have been set. ActionResult<User> actionResult = controller.UpdateUser(user); // First, test action result. actionResult.Should().NotBeNull(); actionResult.Result.Should().NotBeNull(); actionResult.Result.Should().BeAssignableTo<NotFoundObjectResult>(); // Next, test response specific result. NotFoundObjectResult? result = actionResult.Result as NotFoundObjectResult; result.Should().NotBeNull(); // See example handling 400 Bad Request.
Controller method POST returns HTTP status code 409 Conflict:
// Assume that all dependencies have been set. ActionResult<User> actionResult = controller.CreateUser(user); // First, test action result. actionResult.Should().NotBeNull(); actionResult.Result.Should().NotBeNull(); actionResult.Result.Should().BeAssignableTo<ConflictObjectResult>(); // Next, test response specific result. ConflictObjectResult? result = actionResult.Result as ConflictObjectResult; result.Should().NotBeNull(); // See example handling 400 Bad Request.
Mock AppSettings:
// The following dictionary mimics appsettings.json file. // Notice how array values must be defined using indexes. Dictionary<string,string?> configSettings = new() { {"ServiceA:ValueSettingX", "ValueX"}, {"ServiceA:ValueSettingY", "ValueY"}, {"ServiceA:ValueSettingZ", "ValueZ"}, {"ServiceA:ArraySetting1:0", "Value0"}, {"ServiceA:ArraySetting1:1", "Value1"}, {"ServiceA:ArraySetting1:2", "Value2"}, } IConfiguration config = new ConfigurationBuilder() .AddInMemoryCollection(configSettings) .Build();
Common FluentAssertions:
// Value should not be null. value.Should().NotBeNull(); // Value should be of specific type. value.Should().BeOfType<User>(); // Value should be equal to. value.Should().Be(1); value.Should().Be(true); value.Should().Be("expected value"); // Value should contain (comparison is case sensitive). value.Should().Contain("value"); // Value should contain any one of the specified values (comparison is case sensitive): value.Should().ContainAny("value1", "value2"); // Value should contain all of the specified values (comparison is case sensitive): value.Should().ContainAll("value1", "value2"); // String value should be equal to (comparison is case insensitive). value.Should().BeEquivalentTo("Value");