Skip to content

kovac031/LearningUnitTesting

Repository files navigation

LearningUnitTesting

  • C#
  • .NET 7, .NET Framework 4.7.2
  • xUnit, MSTest
  • FakeItEasy, Moq

Learning to do unit tests, using old project copies for testing:

Challenges and learning points

  • learning how to write tests for CRUD operations
  • using tutorials and finding examples to learn from
  • xUnit and MSTest differences
  • FakeItEasy and Moq differences
  • resolving Entity Framework issues
    • occassional conflicts with AutoMapper
    • occassional conflicts with async methods
  • test naming convention
  • optimizing test files structure
  • learning what makes sense to test and where
  • utilizing helper classes
  • DRY practices
    • calling GetFakeStudents() from outside
  • wrote 124 tests overall, did not test all parameters

Comparing initial setup

xUnit and MSTest both require DI-like setup for tests to work

FakeItEasy and Moq syntax slightly different

.NET 7 MVC controller test initialization, using xUnit and FakeItEasy:

public class ListWithParamsTests
    {
        private readonly StudentController _controller;
        private readonly IService _service;
        private readonly IMapper _mapper;

        private readonly ITestOutputHelper _output;

        public ListWithParamsTests(ITestOutputHelper output)
        {
            _service = A.Fake<IService>(); //FakeItEasy
            _mapper = A.Fake<IMapper>();
            _controller = new StudentController(_service, _mapper); // SUT, system under test
            _output = output;
        }
...

.NET Framework MVC controller test initialization, using MSTest and Moq:

[TestClass]
public class ControllerTests
    {
        private DefaultController _controller;
        private Mock<IService> _service;
        private IMapper _mapper;

        [TestInitialize] 
        public void Setup()
        {
            _service = new Mock<IService>();

            MapperConfiguration config = new MapperConfiguration(cfg =>
            {
                cfg.AddProfile(new MappingProfile());
            });
            _mapper = config.CreateMapper();

            _controller = new DefaultController(_service.Object, _mapper); 
        }
...

.NET 7 WebApi controller test initialization, using xUnit and FakeItEasy:

public class StudentControllerTests
    {
        private readonly StudentController _controller;
        private readonly IService _service;
        public StudentControllerTests()
        {
            _service = A.Fake<IService>(); //FakeItEasy
            _controller = new StudentController(_service); // SUT, system under test
        }
...

.NET Framework WebApi controller test initialization, using MSTest and Moq:

[TestClass]
public class GetAllAsyncTests
    {
        private DefaultController _controller;
        private Mock<IService> _service;

        [TestInitialize]
        public void Setup()
        {
            _service = new Mock<IService>();
            _controller = new DefaultController(_service.Object);

            // http fore za webapi controller, toga nema u .net 7 verziji
            _controller.Request = new HttpRequestMessage();
            _controller.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
        }
...

Comparing syntax

syntax for calling the system under test different

xUnit has:

List<StudentDTO> fakeStudents = GetFakeStudents();
A.CallTo(() => _service.GetAllAsync()).Returns(fakeStudents);

MSTest has:

List<StudentDTO> fakeStudents = GetFakeStudents();
_service.Setup(x => x.GetAllAsync(null)).ReturnsAsync(fakeStudents);

Including custom information in test result

adding custom information to test result or debugging different syntax

xUnit uses ITestOutputHelper which requires injecting first:

_output.WriteLine($"Passed from repository - FirstName: {student.FirstName}");

MSTest is simpler, just uses Console.WriteLine, no DI:

Console.WriteLine($"Passed from repository - FirstName: {student.FirstName}");

DbSet in repository

Unit tests in the repository require mocking a DbSet, to be used when calling the mock DBcontext

xUnit, FakeItEasy:

private DbSet<T> CreateFakeDbSet<T>(IQueryable<T> data) where T : class
        {
            IQueryable<T> queryable = data;

            DbSet<T> fakeDbSet = A.Fake<DbSet<T>>(d => d.Implements(typeof(IQueryable<T>))
                                                      .Implements(typeof(IAsyncEnumerable<T>)));
            A.CallTo(() => ((IQueryable<T>)fakeDbSet).Provider).Returns(new TestAsyncQueryProvider<T>(queryable.Provider));
            A.CallTo(() => ((IQueryable<T>)fakeDbSet).Expression).Returns(queryable.Expression);
            A.CallTo(() => ((IQueryable<T>)fakeDbSet).ElementType).Returns(queryable.ElementType);
            A.CallTo(() => ((IQueryable<T>)fakeDbSet).GetEnumerator()).Returns(data.GetEnumerator());
            //
            A.CallTo(() => ((IAsyncEnumerable<T>)fakeDbSet).GetAsyncEnumerator(A<CancellationToken>._)).Returns(new TestAsyncEnumerator<T>(data.GetEnumerator()));

            return fakeDbSet;
        }

MSTest, Moq:

public static Mock<DbSet<T>> CreateMockDbSet<T>(List<T> sourceList) where T : class
        {
            var mockSet = new Mock<DbSet<T>>();
            mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(sourceList.AsQueryable().Provider);
            mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(sourceList.AsQueryable().Expression);
            mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(sourceList.AsQueryable().ElementType);
            mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(sourceList.AsQueryable().GetEnumerator());
            return mockSet;
        }

How to organize tests

the goal is to keep things clean and easily readable

What I did in WebAPI projects: scr1

on the left, each method gets its own tests class

  • more file clutter, but more neatly organized

on the right, all tests are within their layer tests class

  • less file clutter, but one class can end up with hundreds of lines of code, which is not easy to navigate

What I did in MVC projects: scr2

I focused on writing controller tests, as I did service and repository layer tests in the WebAPI projects

on the left, each method gets its own tests class

  • easier to read and navigate

on the right, all tests are within the ControllerTests class

  • less file clutter, but all 15 tests - all testing different methods - are in the same file, which is not ideal

About

Learning unit testing on my .NET MVC and WebApi apps

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published