top of page

C# 9.0 : Record Type Simplifies Unit Testing



Microsoft has released C# 9 with .NET 5.0. It has lots of new features. One feature I am particularly excited about is new Record Type in C# 9. We can now define our data classes with less code, and it looks clean.


It also provides synthesized methods to provide value semantics for equality. Which makes unit test verification lot easier. Without the Record Type, if we use Class to define entity and when a new instance is created in a service under test to pass it to another class’s method, then we need to use It.Is<T>(...) to provide which field values are equal in these instances or write Mock Verification to verify correct instance of the object was passed as parameter.


In this article I have described how C# 9.0 Record Type simplifies unit testing and mocking the dependencies.


Scenario:

Let’s consider a scenario where we have an API endpoint to create new Person in PersonsController. This endpoint takes a NewPersonRequest as parameter from post body. It then calls InsertPerson method of PersonService class.

InsertPerson method applies some business logic and transforms NewPersonRequest into Person database entity and send it to InsertPersonInD method of PersonRepository class, to insert it in the database. Repository method returns a Person with new Id if it successfully inserts it in the database. It returns null if not able to insert it.

InsertPerson method transforms Person in the PersonResponse and sends back to controller method. Which returns result to client. We want to write a unit test for PersonService class's InsertPerson method.

Note: Make sure Projects are using .NET 5.0 Framework.


Now Let’s Discuss Code:


Persons Controller:


[Route("api/[controller]")]    [ApiController]
public class PersonsController : ControllerBase    
{
    private readonly IPersonService _personService;
    public PersonsController(IPersonServicepersonService)        
    {
        _personService=personService;        
    }        
    [HttpPost]
    public IActionResult InsertPerson([FromBody] 
        NewPersonRequest newPersonRequest, 
        CancellationToken cancellationToken)        
    {
    var personResult =_personService.InsertPerson(newPersonRequest);
        
    if (personResult.Success)            
    {
        return Ok(personResult.Entity);            
     }
     
     return BadRequest(personResult);        
   }    
}


Person Class:


public class Person    
{
    public Person(string firstName, string lastName, DateTime dateOfBirth)        
    {
        FirstName=firstName;
        LastName=lastName;
        DateOfBirth=dateOfBirth;        
    }
    
    public Guid Id { get; set; }
    public string FirstName { get; }
    public string LastName { get; }
    public DateTime DateOfBirth { get; }    
 }


New Person Request Class:


public class NewPersonRequest    
{    
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime
    DateOfBirth { get; set; }    
}


Person Response:


public class PersonResponse
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public bool Success { get; set; }
    public string Message { get; set; }
 }


Person Service Interface:


public interface IPersonService    
{
    PersonResponse InsertPerson(NewPersonRequest personRequest);    }


Person Service Implementation


public class PersonService : IPersonService    
{
    private readonly IPersonRepository _personRepository;
    
    public PersonService(IPersonRepositorypersonRepository)        
    {
        _personRepository=personRepository;        
    }
    
   public PersonResponse InsertPerson(NewPersonRequest personRequest)        
   {
       var personToInsert = new Person(
               personRequest.FirstName, 
               personRequest.LastName, 
               personRequest.DateOfBirth);
  
       var person =_personRepository.InsertPersonInDbStore(personToInsert);
       
       if(person==null)            
       {
           return new PersonResponse 
           { 
               Success=false, 
               Message="Unable to insert person in the database" 
            };            
        }
        
        return new PersonResponse            
        {
            FirstName = person.FirstName,
            LastName = person.LastName,
            DateOfBirth = person.DateOfBirth,
            Id = person.Id,
            Success = true            
        };        
    }    
 }


Person Repository Interface


public interface IPersonRepository
{
    Person InsertPersonInDbStore(Person person);
 }


Person Repository Implementation


public class PersonRepository : IPersonRepository
{
    public Person InsertPersonInDbStore(Person person)    
    {
        person.Id = Guid.NewGuid();
        // Code to insert Person in database. 
        return person;    
    }
}


Person Service Test class

using System;
using Blog.CSharp9.RecordTypeTest.Contract;
using Blog.CSharp9.RecordTypeTest.Entity;
using Blog.CSharp9.RecordTypeTest.Repository;
using Blog.CSharp9.RecordTypeTest.Services;
using Moq;
using Xunit;
namespace Blog.CSharp.RecordType.Test
{
    public class PersonServiceTests    
    {
        private MockRepository_mockRepository        
        privateMock<ipersonrepository> _mockPersonRepository;
        public PersonServiceTests()        
        {
        _mockRepository = new MockRepository(MockBehavior.Strict);
        _mockPersonRepository = _mockRepository.Create 
                                    <ipersonrepository>();        
        }
        private PersonService CreateService()        
        {
            return new PersonService(
                _mockPersonRepository.Object);        
        }        
        [Fact]
        public void ShouldInsertPerson()        
        {
            var firstName ="Test";
            var lastName ="Person";
            var dob = new DateTime(1998, 1, 31);
            var id = Guid.NewGuid();
            var emptyGuid = Guid.Empty;
            // Arrange
            NewPersonRequest personRequest = new NewPersonRequest            
            {
                FirstName = firstName,
                LastName = lastName,
                DateOfBirth = dob            
            };
            var newPerson = new Person(firstName, lastName, dob)            
            {
                Id = emptyGuid,            
            };
            var expectedPerson = new Person(firstName, lastName, dob)            
            {
                Id=id,            
            };
           _mockPersonRepository.Setup(x=>x.InsertPersonInDbStore(newPerson)).Returns(expectedPerson);
            var service=CreateService();
            // Act
            var result = service.InsertPerson(
                personRequest);
            // Assert
            Assert.True(result.Success);
            Assert.Equal(firstName, result.FirstName);
            Assert.Equal(lastName, result.LastName);
            Assert.Equal(dob, result.DateOfBirth);
            _mockPersonRepository.Verify();        
        }    
    }
}


Debug the Test:

Let’s now debug the test. Right click on the test method and select ‘Debug Test’ from the context menu. First break point is on the line where we are adding a mock setup for InsertPersonInDbStore method. We are setting up these values for newPerson variable which should be passed as a parameter for this method.



Press F5 key to continue debugging. Next it would throw an exception in the InsertPerson method of PersonService class where it calls to InsertPersonInDbStore. Exception says that the Mock setup can not be found for this method. Although we have done one.



If we hover over the instance then it shows same values for every fields same as the instance created for Mock setup.



So what’s the problem here? It is because the Person instance was created in Mock and instance created here, does not reference to same memory location and hence .NET does not think they are the same instances.

We can solve this issue by changing Mock setup as below:

_mockPersonRepository.Setup(x=>x.InsertPersonInDbStore(It.Is<person>(p=>p.DateOfBirth==dob
    && p.FirstName == firstName
    && p.LastName == lastName
    && p.Id == emptyGuid)))                
 .Returns(expectedPerson);

This setup will be fine if there are handful of fields, but what if we have 10 or 15 fields and we have to write lots of unit tests, then we have to implement IEquatable<Person> interface in the Personclass.


Let’s see how Record type solves this issue without any extra work. Change Person class to record as below:

public record Person(string FirstName, string LastName, DateTime DateOfBirth)
{
    publicGuidId { get; set; }
}

Or we can also declare it in traditional way

public record Person    
{
    public Person(string firstName, string lastName, DateTime 
    dateOfBirth)        
    {
        FirstName=firstName;
        LastName=lastName;
        DateOfBirth=dateOfBirth;        
    }
    
    public Guid Id { get; set; }
    public string FirstName { get; }
    public string LastName { get; }
    public DateTime DateOfBirth { get; }    
}

Now run the Test again, and this time it passes without any issue.



Summary:

As we can see record type reduces not only coding time and number of lines, but also provides many other benefits.



Source: Medium


The Tech Platform

0 comments

Comments


bottom of page