Close

Run Unit Tests via Azure DevOps

Two important unit tests you might want to perform in your Azure project are related to maps and reusable functions. Projects for map testing are written in plain .Net. Projects for Azure Functions and function testing are written in .Net Core.

In DevOps unittest steps are included in the build pipeline, not in the release pipeline. First build your test project, then either run UnitTests.Net or UnitTests .Net Core.

After building the unittest project, you will have a project assembly (dll). That’s why you select drop down option “Test assemblies”. The other properties are quite self explaining and are the same for .Net and .Net Core teststeps:

Now let’s look at the test projects in Visual Studio.

Test Map:

  • NuGet: MSTest.TestAdapter, MSTest.TestFramework, XMLDiffPatch
  • Using: TestResources (input/outputfiles), Microsoft.VisualStudio.TestTools.UnitTesting, System.IO, System.Text, System.Xml, System.Xml.Xsl;

All tests use a function named TransformHelper:

private static XmlDocument TransformHelper(string input, string relatienummer)
        {
            var settings = new XsltSettings();
            settings.EnableScript = true;
            var map = new XslCompiledTransform();
            map.Load("../../../../IntegrationAccount/Maps/map.xslt", settings, null);

            XsltArgumentList parameters = new XsltArgumentList();
            parameters.AddParam("Relatienummer", "", relatienummer);

            XmlReader reader = XmlReader.Create(new StringReader(input));
            reader.Read();

            StringBuilder resultString = new StringBuilder();
            XmlWriter writer = XmlWriter.Create(resultString);

            map.Transform(reader, parameters, writer);

            return resultString.ToString().ToXmlDocument();
        }

An example test method looks as follows: First of all you see the testmethod is annotated with string [TestMethod]. Then helper method TransformHelper is called. Finally, the XmlTestBench is used to check for NodeHasValue or NodeIsNotPresent if you like. XmlTestBench is part of a custom assembly, but obviously you can change the code to do your own assert.

[TestMethod]
public void AssertAansteldatumTransforms()
{  
	var output = TransformHelper(IntermediairBericht, "0000000002");
        Assert.IsNotNull(output);
        Assert.IsTrue(XmlTestBench.NodeHasValue(output, "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='aansteldatum']", "1994-10-10T00:00:00"), "Aansteldatum in ouput klopt niet");
}

Test Function:

  • NuGet: coverlet.connector, Microsoft.AspNetCore.Mvc, Microsoft.Net.Test.Sdk, xunit, xunit.runner.visualstudio

In this case the function tested gets a Json document as an input and checks if the MSDynamics RelationId (key value) is available. The function tests make use of the custom classes: TestFactory and ListLogger. ListLogger in turn uses LoggerTypes and NullScope.

Class TestFactory

  • Using: Microsoft.AspNetCore.Http, Microsoft.AspNetCore.Http.Internal, Microsoft.Extensions.Logging, Microsoft.Extensions.Logging.Abstractions, System.IO;
namespace IntermediairIn.FunctionApp.UnitTests
{
    public class TestFactory
    {
        public static DefaultHttpRequest CreateHttpRequest(string body)
        {
            var request = new DefaultHttpRequest(new DefaultHttpContext())
            {
                Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(body))
            };
            return request;
        }

        public static ILogger CreateLogger(LoggerTypes type = LoggerTypes.Null)
        {
            ILogger logger;

            if (type == LoggerTypes.List)
            {
                logger = new ListLogger();
            }
            else
            {
                logger = NullLoggerFactory.Instance.CreateLogger("Null Logger");
            }

            return logger;
        }
    }
}

Class ListLogger

  • Using: Microsoft.Extensions.Logging, System, System.Collections.Generic;
namespace IntermediairIn.FunctionApp.UnitTests
{
    public class ListLogger : ILogger
    {
        public IList<string> Logs;

        public IDisposable BeginScope<TState>(TState state) => NullScope.Instance;

        public bool IsEnabled(LogLevel logLevel) => false;

        public ListLogger()
        {
            this.Logs = new List<string>();
        }

        public void Log<TState>(LogLevel logLevel,
                                EventId eventId,
                                TState state,
                                Exception exception,
                                Func<TState, Exception, string> formatter)
        {
            string message = formatter(state, exception);
            this.Logs.Add(message);
        }
    }

    public enum LoggerTypes
    {
        Null,
        List
    }

    public class NullScope : IDisposable
    {
        public static NullScope Instance { get; } = new NullScope();

        private NullScope() { }

        public void Dispose() { }
    }

}

Unit Test

  • Using FunctionApp.GetMsdynamicsKey, Microsoft.AspNetCore.Mvc, Microsoft.Extensions.Logging, Newtonsoft.Json, Xunit
public class GetMsdynamicsKeyUnitTest
    {
        private readonly ILogger logger = TestFactory.CreateLogger();

        [Fact]
        public async void CDHKeyFound()
        {
            var request = TestFactory.CreateHttpRequest("{\"revision\":213842,\"records\":[{\"source\":\"sysa\",\"key\":\"9999999\", ... }");
            var response = (OkObjectResult)await GetMsdynamicsKey.Run(request, logger);
            var expected = "{\"key\":\"AA999...A\"}";
            Assert.Equal(JsonConvert.DeserializeObject(expected), response.Value);
        }

Note that annotation [Fact] is used instead of [TestMethod]. The function is named GetMSDynamicsKey. The method name is the standard name for functions: Run.