C# standardizing tests - Part 5

Creating tests

We have created in part 4 of this series of posts, the implementation of the tests that cover both the happy path and the wrong path of the constructor method of our MetadataService class.

Now we are going to know the implementation of the Generate() method and we will cover 100% of our MetadataService with tests.

MetadataService Class

The MetadataService class has only one responsibility, to generate a section of XML that will be included in a larger XML.

This section contains different data that are not relevant to know.

What we will need to identify are all the flows of your implementation to cover them with tests. Let's get to know our MetadataService.

using ...;

namespace ...
{
    public class MetadataService : IMetadata
    {
        #region Attributes

        private readonly IUtilities _utilities;
        private readonly IUrlUtils _urlUtils;
        private readonly IQRImageBuilder _qrBuilder;

        #endregion

        #region Constructor Methods

        public MetadataService(IUtilities utilities,
                               IUrlUtils urlUtils,
                               IQRImageBuilder qrBuilder)
        {
            _utilities = utilities ?? throw new ArgumentNullException(nameof(utilities));
            _urlUtils = urlUtils ?? throw new ArgumentNullException(nameof(urlUtils));
            _qrBuilder = qrBuilder ?? throw new ArgumentNullException(nameof(qrBuilder));
        }

        #endregion

        #region Public Methods

        public void Generate(ref IResponse response, XmlDocument xml)
        {
            if (xml is null)
            {
                throw new ArgumentNullException(nameof(xml));
            }

            var identifier = GenerateIdentifier(xml);

            if(identifier.HasErrors)
            {
                response.Issues.AddRange(identifier.Issues);
                return;
            }

            var qrCode = GenerateQrCode(identifier.Item, xml);

            if (qrCode.HasErrors)
            {
                response.Issues.AddRange(qrCode.Issues);
                return;
            }

            response.MetaData = new Metadata
            {
                Identifier = identifier.Item.Identifier,
                QRCode = qrCode.Item.codeQR,
                QRImage = _qrBuilder.GenerateImage(qrCode.Item.codeQR)
            };
        }

        #endregion

        #region Private Methods

        private IIdentifier GenerateIdentifier(XmlDocument xml)
        {
            var signature = xml.GetElementsByTagName("ds:SignatureValue")[0].InnerText;

            return _Utilities.GetIdentifier(signature);
        }

        private ICodeQR GenerateQrCode(IIdentifier identifier, XmlDocument xml)
        {
            var serie = xml.GetElementsByTagName("Serie")[0].InnerText;
            var qr = GetQr(_urlUtils.QrUrl, identifier.Id, serie);

            return _Utilities.GetQrCode(qr);
        }

        private IQr GetQr(string url, string id, string serie)
        {
            return new Qr
            {
                URL = url,
                Identifier = id,
                Serie = serie,
            };
        }

        #endregion
    }
}

As we can see, our MetadataService class has only one public method called Generate() that receives by parameter an object with the ref attribute called "response" and an "XML" that already has data.

The other methods are private and use the services injected in the constructor of the MetadataService class.

We see that there is no try-catch as such to catch errors or manage them. In this case, it is understood that services such as the _Utilities will have their test battery and will validate when the received parameter is null.

We will for the moment define the expected responses of that service because as I mentioned before, it is supposed to be already tested.

Before we start, I will recall the distribution of folders and test files mentioned in Part 1

  • ServicesTests

    • MetadataService

      • Fakes

        • QueryBuilderFake (Example)
      • Fixtures

        • MetadataServiceFixture
      • Mocks

        • MetadataServiceMocks
      • Implementations

        • MetadataServiceTests

Happy path test in the Generate(...) method

Let's start by defining the name of the test, and the sections corresponding to the triple-A explained in previous posts.

[Fact]
public void MetadataService_Generate_AllParametersSent_ReturnOk()
{
    //Arrange
    ...

    //Action
    ...

    //Assert
    ...
}

To be able to cover the test flow with all the parameters and services ok, first, we need that the services used within the test are configured and second, to check that the generated data have the data that we will send to the XML.

The first point, we can solve in several ways, one can be to create a Fake class configured with what we need and pass it by parameter to our fixture service to send it to the class when creating the instance.

But our class does not have only one service injected by a parameter, it has 3. Therefore, we are going to create a way to delegate the configuration responsibility of the services to the fixture class, which allows us to create Mock instances of the services or create a fake class for each one of them if we want to work that way.

In the Fixture Service class, we will create a method that initializes the services and then, we will pass those initialized services to another method that we will also create inside the fixture to configure the services for the happy path. But if we have a test that must configure some service returning an error, we create another setup method to configure it and force the desired error.

///In the fixture service class, we add this method
public void InitializeServices(out Mock<IUtilities> utilities,
                               out Mock<IUrlUtils> urlUtils,
                               out Mock<IQRImageBuilder> imageBuilder)
{
    utilities = new Mock<IUtilities>();
    urlUtils = new Mock<IUrlUtils>();
    imageBuilder = new Mock<IQRImageBuilder>();
    //We could create a fake instance by passing in the desired byte[].
    //    imageBuilder = new QrBuilderFake(new byte[]);
}

As we can see in the previous code, the method receives a Mock object for each service, but the out attribute indicates that it is a service that can be used externally by whoever invokes this method.

This allows us to initialize the services with Mock or to create an instance of a fake class and then pass them to the corresponding configuration test.

What is a fake class? A fake class is a class that inherits the service interface and implements, apart from the methods of its interface, an alternative to passing the result we want to obtain from its methods.

In this case, we have created a private variable that through the constructor we initialize, Then, calling the method would return that value.

Here I leave an example:

public class QrBuilderFake : IQRImageBuilder
{
    #region Attributes    
    private readonly byte[] _image;
    #endregion

    #region Constructor Methods
    public QrBuiderFake(byte[] imageResult)
    {
        _image = imageResult;
    }
    #endregion

    #region Public Methods
    public byte[] GenerateImage(string code)
    {
        //if you want you can handle code value before return _image
        return _image;
    }
    #endregion
}

Services Configuration

Having the services initialized, according to each scenario we want to test, they can be configured to return the expected data or responses.

To achieve this from the fixture we will create a method called "SetupServicesOk" for the happy path, passing by parameter the services that must be configured for a correct result.

 public void SetupServicesOk(ref Mock<IUtilities> utilities, ref Mock<IQRImageBuilder> qrBuilder, ref Mock<IUrlUtils> urlUtils)
{
    utilities.Setup(s => s.GetIdentifier(It.IsAny<string>())).Returns(MetadataServiceMock.GetIdentifierOK());
    utilities.Setup(s => s.GetQrCode(It.IsAny<IQr>())).Returns(MetadataServiceMock.GetQrCodeOk());
    qrBuilder.Setup(s => s.GenerateImage(It.IsAny<string>())).Returns(MetadataServiceMock.GetImageOk());
    urlUtils.Setup(s => s.QrUrl()).Returns(MetadataServiceMock.GetUrlOk());
}

As you can see above, the services needed to obtain the data are configured, and if you have noticed, we are calling in each return of each configuration a service that is responsible for generating the mock data called "MetadataServiceMock".

This is created for each service to test as well as the fixture as can be seen in the distribution of test folders above, in this way there will be no shared data between tests and we will not have couplings.

The MetadataServiceMock class will be static and we will create in it all the desired Mock for each test scenario.

With these services configured, we can now create our Happy Path test.

[Fact]
public void MetadataService_Generate_AllParametersSent_ReturnOk()
{
    //Arrange
    this.fixture.InitializeServices(out Mock<IUtilities> utilities, out Mock<IUrlUtils> urlUtils, out Mock<IQRImageBuilder> qrImage);

    this.fixture.SetupServicesOk(ref utilities, ref qrImage, ref urlUtils);

    this.SetService(utilities.Object);
    this.SetService(qrImage.Object);
    this.SetService(urlUtils.Object);

    var result = this.fixture.GetResult();
    var xml = this.fixture.GetXMLOK();

    var sut = this.fixture.GetSut();

    //Action
    sut.GenerateMetadata(ref result, xml);

    //Assert
    Assert.NotNull(result);
    Assert.NotNull(result.MetaData);
    Assert.NotNull(result.MetaData.Identifier);
    Assert.NotNull(result.MetaData.QRCode);
    Assert.NotNull(result.MetaData.QRImage);
}

The GetResult() and GetXMLOk() methods are inside the fixture and call the Mock service to return the Mock object for its respective use.

In methods such as GetResult() you do not return a Mock object but an instance of an object, but the data inside it can be data managed directly in the Mock, it is your responsibility to create it.

For example:

public static class MetadataServiceMock
{
    //Return data mock
    public static byte[] GetImageOk()
    {
        return new byte[] { 1, 2 };
    }

    //Return new instance object
    public static Result<BuilderResult> GetResult()
    {
         return new Response<BuilderResult> 
         { 
             Issues = new List<IIssue>() 
         };
    }

    ...
}

XML null test into Generate(...) method

The XML is passed as a parameter to the method, and in the method flows we can see that an argument null exception would be returned if the XML arrives null. Let's see what the test would look like.

[Fact]
public void MetadataService_Generate_XMLNull_ReturnException()
{
    //Arrange
    var result = this.fixture.GetResult();    
    var sut = this.fixture.GetSut();

    //Action Assert
    Assert.Throws<ArgumentNullException>(() => sut.Generate(ref result, null));
}

An identifier with errors test into Generate (...) method

The second flow of the test receives an identifier that can have errors, the identifier is generated by the utility service so we only have to initialize and configure that service to generate an error, the other services are left with the "_" because they will not be used.

Let's see how the test looks like

[Fact]
public void MetadataService_Generate_IdentifierWithErrors_ReturnException()
{
    //Arrange
    this.fixture.InitializeServices(out Mock<IUtilities> utilities, out Mock<IUrlUtils> _, out Mock<IQRImageBuilder> _);

    this.fixture.SetupIdentifierWithError(ref utilities);

    this.fixture.SetService(utilities.Object);

    var result = this.fixture.GetResult();
    var xml = this.fixture.GetXMLOK();

    var sut = this.fixture.GetSut();

    //Action
    sut.Generate(ref result, xml);

    //Assert
    Assert.NotNull(result);
    Assert.True(result.Issues.Any());
}

And the new SetupIdentifierWithError(ref utilities) method, how does it look in the fixture? Let's see it now

public void SetupIdentifierWithError(ref Mock<IUtilities> utilities)
{
    utilities.Setup(s => s.GetIdentifier(It.IsAny<string>())).Returns(MetadataServiceMock.GetIdentifierWithError());
}

And the GetIdentifierWithError() method in the Mock service would look like this:

public static NonQueryResponse<IdentifierTBAI> GetIdentifierWithError()
{
    return new NonQueryResponse<IdentifierTBAI>
    {
        Issues = GetIssueWithError()
    };
}

...

private static List<IIssue> GetIssueWithError()
{
    return new List<IIssue>()
    {
        new Issue
        {
            Code = "Error",
            Message = "Error",
            Type = IssueType.Error
        }
   };
}

With that, we have covered almost all the flows, and we only have one task left, create the test to cover the flow where the QrCode has errors.

If we look at our MetadataService the QrCode is also generated by our utility service. So the process is the same. Let's create the test.

[Fact]
public void MetadataService_Generate_QrCodeWithErrors_ReturnException()
{
    //Arrange
    this.fixture.InitializeServices(out Mock<IUtilities> utilities, out Mock<IUrlUtils> _, out Mock<IQRImageBuilder> _);

    this.fixture.SetupQrCodeWithError(ref utilities);

    this.fixture.SetService(utilities.Object);

    var result = this.fixture.GetResult();
    var xml = this.fixture.GetXMLOK();

    var sut = this.fixture.GetSut();

    //Action
    sut.GenerateMetadata(ref result, xml);

    //Assert
    Assert.NotNull(result);
    Assert.True(result.HasErrors);
}

Let's look at the configuration method inside the fixture called "SetupQrCodeWithError".

public void SetupQrCodeWithError(ref Mock<IUtilities> utilities)
{
    utilities.Setup(s => s.GetIdentifier(It.IsAny<string>())).Returns(MetadataServiceMock.GetIdentifierOK());
    utilities.Setup(s => s.GetQrCode(It.IsAny<IQr>())).Returns(MetadataServiceMock.GetQrCodeWithError());
}

As we can see we have configured the first GetIdentifier service to return a correct object but we are configuring the GetQrCode to fail.

Let's see the error configuration for that method generated by the MetadataServiceMock class.

public static NonQueryResponse<CodeQRTBAI> GetQrCodeWithError()
{
    return new NonQueryResponse<CodeQRTBAI>
    {
        Issues = GetIssueWithError() //Reutilizamos este método
    };
}

Conclusión

Delegating responsibilities in the tests is feasible. If we create the fixture that will be injected into the test class thanks to the ClassFixture, then we create the Mock class and finally, if necessary, we create the fake class.

For each service, we can create 100% coverage using this methodology, in the test of the constructor methods we can use a ClassData to pass as a parameter all the possible combinatorics of each injected service.

In each method of the class to test we must cover with test each line of code, and if we see that the data inside the method to test comes from an external service we can Mocke them or create fake classes.

If we implement unit tests for each service, we will reach a point where the coverage of our project will be 100%.

One last recommendation or advice: In case an error occurs in Production, the most important thing is to first create a test that simulates that error in the identified service to have the scenario covered. Then, we solve the error and modify the previously created test so that we can control the expected behavior in case that particular and distant scenario occurs again.

So much for the end of this series. I hope it has been of some use to you. See you next time...

0
Subscribe to my newsletter

Read articles from Jaime Andrés Quiceno González directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jaime Andrés Quiceno González
Jaime Andrés Quiceno González

I am motivated Software and Telecommunications Engineer with 9 years of experience in analysis, design, implementation, testing and deployment of software solutions. I am passionate about knowing the context of the problem in detail to end an adequate solution applying SOLID principles and design patterns to deliver software products with quality and efficiency. I have supported and innovated with automation various business and industrial processes through several programming languages and work frameworks with and without Agile methodologies. I have skills and knowledge in both FrontEnd and BackEnd.