I often hear developers saying they’d like to write tests or when was the last time they wrote unit test. That’s why I decided it’s good idea to write post about “how to” unit testing. You’ll learn how to write tests with good practices, how to make tests independent from another classes. I’ll show you couple of examples for controller, service and tests with EF Core.
3A
Every test should contains 3 sections, arrange, act, assert or given, when, then. It doesn’t matter how you’ll call it. It’s good practice to start unit test by adding 3 comments for every section, from there writing your test.
Arrange
Arrange is used for test setup. You should create every needed object here. Additionally here you should add mocks for dependencies.
It is possible that you’ll be able to setup all of your tests in one place like TestInitialize in MSTest or Contructor in XUnit. In that case it’s ok to ditch arrange section or you can leave just comment and start from act section.
Act
Act is used to call tested method. Additionally you should dispose objects or use using statement in this section. After act section test is ended, so you should clean up after yourself.
Assert
Assert is heart of your test. Here you can validate results. By default you can use one of Assert methods, but at the end of post I’ll show you another way to assert results.
Controller Tests
First of all, why should you write unit tests for controllers? To be perfectly honest you don’t need them. If you’re creating clean controllers without logic, then your tests will be use only as documentation. So no one will change status codes by mistake or change returned type without thinking.
Personally I don’t create those test, but I’ll show you example if you would need one. In my opinion it’s enough if you create only integration test that will go thru happy path, so you can make sure everything works fine without running application yourself. We’ll cover integration test from client to database in next post.
private readonly Mock<IStudentRepository> repository;
private readonly Mock<IMapper> mapper;
private readonly Mock<ILogger<StudentsController>> logger;
private readonly Mock<IAuthorizationService> authorizationService;
public StudentControllerTest()
{
repository = new Mock<IStudentRepository>();
mapper = new Mock<IMapper>();
logger = new Mock<ILogger<StudentsController>>();
authorizationService = new Mock<IAuthorizationService>();
}
[Fact]
public async Task ShouldReturnOneStudent()
{
// arrange
repository.Setup(x => x.GetByIdAsync(It.IsAny<int>()))
.Returns(GetStudent());
authorizationService.Setup(x => x.AuthorizeAsync(It.IsAny<ClaimsPrincipal>(), It.IsAny<Student>(), It.IsAny<string>()))
.ReturnsAsync(AuthorizationResult.Success());
mapper.Setup(x => x.Map<Student, StudentResponseDataTransferObject>(It.IsAny<Student>()))
.Returns(await GetStudentResponse());
var controller = new StudentsController(repository.Object, mapper.Object, logger.Object, authorizationService.Object);
// act
var result = await controller.GetStudent(It.IsAny<int>());
var okResult = result.Result.As<OkObjectResult>();
var student = okResult.Value.As<StudentResponseDataTransferObject>();
// assert
okResult.StatusCode.Should().Be(StatusCodes.Status200OK);
student.Should().BeEquivalentTo(await GetStudentResponse());
}
Test initialize
In XUnit constructor is used as test initialize. Code inside constructor is called before every test. There is possibility to call initialize once and we will cover it later.
Moq
I’m using Moq to create fake implementations for dependencies of tested class. Reason for that is, we want to test only one thing at the time and make sure that bugs in dependencies won’t affect class we’re currently testing.
Using Moq we can create Mocks easily. If we wouldn’t use mocking package we would need to create new fake class that would implement interface used in class we want to test. So Moq is just implementing it for us on the fly.
By default Moq will implement every method from interface as empty method. If you need mock methods to do something more you can setup mocks like this:
repository.Setup(x => x.GetByIdAsync(It.IsAny<int>()))
.Returns(GetStudent());
We need to use Setup() method. In param we can select which method we want to setup. Additionally we can setup for which params it will work. In my case I didn’t care about params it should work for everything, that’s why I used It.IsAny<int>(), it works the same way for other types.
Finally I’ve used Returns() method to config mock to return result of GetStudent() method that I prepared for tests. You can also use Callback() method to configure you test, so it will do something you specify inside. For more information check Moq tutorial.
Fluent Assertions
I’m using FluentAssertions nuget package for asserting tests. Mainly, because it has multiple methods we can use in assert section. For example it can compare two objects field by field, it is used in line student.Should().BeEquivalentTo(await GetStudentResponse()).
Additionally you can create assertions fluently, it means you can read them as sentence, look at example above. Student should be equivalent to (some object). It’s more friendly than Assert.AreEquals() for every field you want to assert.
Fluent Assertions have so many options, so I could’ve create post only for it. It’s the best to check their documentation. It’s written really well, with examples and all. You’ll be amazed how much work they did for us.
Data Driven Testing
Is way to create tests, so we can use the same test with different input to cover more scenarios. XUnit support it using [Theory] and [InlineData] attributes. How to use it?
[Theory]
[InlineData(null)]
[InlineData(" ")]
[InlineData("")]
public void ThrowExceptionWhenNameIsNotValid(string name)
{
}
You need to specify your inline data like above, it has to be constant, that’s why you’ll need to create different tests for more complex scenarios. XUnit will put your inline data in parameter of test. It is possible to add multiple parameters, all you need to do is add next after coma. Remember to extend method definition with additional parameter as well.
Business Service Tests
Business Logic is code you should cover in unit tests. You should be able to tell if your logic works and validate that any change or new code didn’t break anything.
Test Coverage trap
Recently I’ve heard about problem with test coverage level set statically by company. Developers knowing that they need to cover 80% of application will write a lot of useless tests just to make that 80%.
It often happens that they won’t start from really important tests like business logic. Leaving them for the end, until 80% is reached. Thank to that business logic is not tested at all.
Make sure you won’t get caught in that trap. Always test business logic, because of all places it needs testing for sure.
Tests with Entity Framework
While testing code that uses EF you’ve got 2 options. If you’re using Linq to SQL you can write unit tests using in memory provider. Still you won’t be able to mock linqs but if you write tests for it, you can assume they works and use them without fear that they will mess with your tests.
If you’re writing raw SQLs you have to test your code with Integration Tests, that because you can query in memory provider database only with linq to sql. To be more specific, linq will not be translated to sql at all.
The only difficulty with writing unit tests with EF using in memory provider is to setup in memory database.
Single Test Initialize
For setting up in memory database we’ll use another XUnit feature. Test initializer that will be called once before everything else. You can also do that with other Test Frameworks. For example in MSTest you can achieve this by adding attribute [ ClassInitialize
] to method you want to make initializer.
Unfortunately this method will be limited. In XUnit case it’s far better. You’ll have to create new class that will be initializer.
public class StudentFixture
{
public DbContextOptions<BlogPostContext> Options { get; }
public IEnumerable<StudentCourse> ExpectedMissingGrades { get; private set; }
public StudentFixture()
{
Options = new DbContextOptionsBuilder<BlogPostContext>()
.UseInMemoryDatabase(databaseName: "StudentContextOptions")
.EnableSensitiveDataLogging()
.Options;
SeedDatabase(Options);
}
...
}
All we need to do is create DbContextOptions with in memory provider and make it public for our tests. Additionally we can seed database. This method will call inserts on Student DbContext and SaveChanges.
When you have your initialize class finished you need to let know XUnit that it should use it for initialization. You’re doing it by implementing IClassFixture interface in test class. XUnit will make sure that you’ll have your fixture ready to use in tests.
public class StudentServicesTests : IClassFixture<StudentFixture>
{
private readonly StudentFixture fixture;
private StudentService sut;
public StudentServicesTests(StudentFixture fixture)
{
this.fixture = fixture;
}
...
}
[Fact]
public void Should_ThrowException_When_CourseNotExist()
{
// arrange
const int studentId = 1;
const int notExistingCourseId = 666;
using var dbContext = new BlogPostContext(fixture.Options);
sut = new StudentService(dbContext);
// act
Func<Task> act = async () => await sut.GetWeightedAverageForCourseAsync(notExistingCourseId, studentId);
// assert
act.Should().Throw<ArgumentException>();
}
You can create dbContext with in memory provider using Options from fixture. From that point your dbContext will use in memory database.
Assert throw exception
Example above presents how you can confirm that exception was called during test run.
It’s better way to do that than MSTest, where you need to add attribute ExpectedException
. Problem with this one is, you can’t tell where your exception should be thrown. So it might catch exception from any part of test code and mark it as valid.
In XUnit with FluentAssertions we can specify which method should throw exception. We’re doing it by assigning our method call to variable and then asserting that it will throw exception.
// act
Func<Task> act = async () => await sut.GetWeightedAverageForCourseAsync(notExistingCourseId, studentId);
// assert
act.Should().Throw<ArgumentException>();
Testing Validators
Fluent Validation is gateway inside your application, that’s why it’s important to tests your validator as well. You shouldn’t do that to make sure your validator do what they should do, it’s still important but validation code is most of the time pretty simple. Why really you need test for them is to create documentation for other developers. By adding tests for validator you’ll let them know they shouldn’t change validator without good reason.
[Theory]
[InlineData(null)]
[InlineData(" ")]
[InlineData("")]
public void InvalidWhenNameIsNotValid(string name)
{
validator.ShouldHaveValidationErrorFor(x => x.Name, name);
}
[Fact]
public void ValidWhenNameProvided()
{
const string validName = "Name";
validator.ShouldNotHaveValidationErrorFor(x => x.Name, validName);
}
FluentValidations has special helping methods for testing. It’s not rocker science, methods names are pretty descriptive.
Testing AutoMapper mapping profiles
Finally, you can easy test your auto mapper profiles. As well as FV, AutoMapper has it’s own helping method for testing. AssertConfigurationIsValid will run every mapping within profile and return information about errors. Remember it’s very easy to make easy mistake like this one. You’ll change something with models and forget about mappers. Compiler won’t find anything errors and your application will fail during runtime. Writing those tests is very cheap.
public class StudentMappingTest: IDisposable
{
[Fact]
public void ShouldMapConfigurationBeValid()
{
Mapper.Initialize(m => m.AddProfile<StudentMappingProfile>());
Mapper.AssertConfigurationIsValid();
}
[Fact]
public void ShouldMapStudentCreateDtoToStudent()
{
Mapper.Initialize(cfg => cfg.CreateMap<StudentCreateRequestDataTransferObject, Student>(MemberList.Source));
Mapper.AssertConfigurationIsValid();
}
public void Dispose()
{
Mapper.Reset();
}
}