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"));
A.CallTo(() => httpRequest.QueryString).Returns(new QueryString("?a=b&c=d"));
 
// 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");
 


No comments:
Post a Comment