Summary: Samples of C# code handling common unit testing tasks using xUnit, FakeItEasy, and FluentAssertions frameworks.
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");