Home > Back-end >  Testing of even easy behaviour leads to a jungle of testing functions
Testing of even easy behaviour leads to a jungle of testing functions

Time:04-12

Until today I had a hard time with unit testing. For this reason I just started to read a book enter image description here

Testing code: ~100 lines
Production code: ~30 lines

Really?

using CP.Admin.Core.SDK.ValueObjects;
using DataHive.Validations.Exceptions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;

namespace CP.Admin.Tests
{
    [TestClass]
    public class RoleAssignementTests
    {
        [TestMethod]
        public void RoleAssignementFirst_NullRoleIdParameter_ThrowsArgumentNullException()
        {
            Action useConstructor = () =>
            {
                var roleAssignement = new RoleAssignement(null, "62500ac55988223c8b9b28fc");
            };
            Assert.ThrowsException<ArgumentNullException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementFirst_InvalidRoleIdParameter_ThrowsIdNotValidException()
        {
            Action useConstructor = () =>
            {
                var roleAssignement = new RoleAssignement("invalidId", "62500ac55988223c8b9b28fc");
            };
            Assert.ThrowsException<IdNotValidException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementFirst_NullEnterpriseScopeIdParameter_ThrowsArgumentNullException()
        {
            Action useConstructor = () =>
            {
                string param = null;
                var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", param);
            };
            Assert.ThrowsException<ArgumentNullException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementFirst_InvalidEnterpriseScopeIdParameter_ThrowsArgumentNullException()
        {
            Action useConstructor = () =>
            {
                var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", "invalidId");
            };
            Assert.ThrowsException<IdNotValidException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementSecond_NullRoleIdParameter_ThrowsArgumentNullException()
        {
            Action useConstructor = () =>
            {
                var param = new List<string> { "62500ac55988223c8b9b28fc" };
                var roleAssignement = new RoleAssignement(null, param);
            };
            Assert.ThrowsException<ArgumentNullException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementSecond_InvalidRoleIdParameter_ThrowsIdNotValidException()
        {
            Action useConstructor = () =>
            {
                var param = new List<string> { "62500ac55988223c8b9b28fc" };
                var roleAssignement = new RoleAssignement("invalidId", param);
            };
            Assert.ThrowsException<IdNotValidException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementSecond_NullSiteScopeIdParameter_ThrowsArgumentNullException()
        {
            Action useConstructor = () =>
            {
                List<string> param = null;
                var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", param);
            };
            Assert.ThrowsException<ArgumentNullException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementSecond_InvalidSiteScopeIdParameter_ThrowsIdNotValidException()
        {
            Action useConstructor = () =>
            {
                var param = new List<string> { "invalidId" };
                var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", param);
            };
            Assert.ThrowsException<IdNotValidException>(useConstructor);
        }
        [TestMethod]
        public void RoleAssignementFirst_ParametersAreOkay_AllValuesAreCorrect()
        {
            var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", "62500ac55988223c8b9b28fc");
            Assert.IsNotNull(roleAssignement.RoleId);
            Assert.IsNotNull(roleAssignement.EnterpriseId);
            Assert.IsNull(roleAssignement.SiteIds);
        }
        [TestMethod]
        public void RoleAssignementSecond_ParametersAreOkay_AllValuesAreCorrect()
        {
            var param = new List<string> { "62500ac55988223c8b9b28fc" };
            var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", param);
            Assert.IsNotNull(roleAssignement.RoleId);
            Assert.IsNotNull(roleAssignement.SiteIds);
            Assert.IsNull(roleAssignement.EnterpriseId);
        }
    }
}

CodePudding user response:

As you recognize, going down this road will be very painful. Because wanting to assert for each possible case (every parameter value every possible combination) will require (as you saw) more work than making the actual production code to work.

All of this because you are orienting tests regarding data.

If you consider testing the behavior of the system instead, you can break free from a lot of implementation details and focus on a higher level.

Considering behavior, the only one that I can eventually see is

The other two parameters should be exclusively set (if one is null, the other must be set and vice versa).

It corresponds to scenarii 9 and 10 according to your numerotation:

[TestMethod]
public void RoleAssignementFirst_ParametersAreOkay_AllValuesAreCorrect()
{
    var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", "62500ac55988223c8b9b28fc");
    Assert.IsNotNull(roleAssignement.RoleId);
    Assert.IsNotNull(roleAssignement.EnterpriseId);
    Assert.IsNull(roleAssignement.SiteIds);
}

[TestMethod]
public void RoleAssignementSecond_ParametersAreOkay_AllValuesAreCorrect()
{
    var param = new List<string> { "62500ac55988223c8b9b28fc" };
    var roleAssignement = new RoleAssignement("62500ac55988223c8b9b28fc", param);
    Assert.IsNotNull(roleAssignement.RoleId);
    Assert.IsNotNull(roleAssignement.SiteIds);
    Assert.IsNull(roleAssignement.EnterpriseId);
}

Now the test codebase has a significant smaller proportion than before comparing to the production codebase, which is a better compromise because it tests the most important thing at a greatly reduced price (both in implementation and maintenance).

Going further

Let me allow you to see something that you may never has thought would be possible. RoleAssignment could require no tests and still enforce the same rules as the one you want by better using the type system.

Consider the following code:

public class RoleAssignement
{
    public Id RoleId { get; }
    public Either<Id, List<Id>> RelatedIds { get; }

    public RoleAssignement(Id roleId, Either<Id, List<Id>> relatedIds)
    {
        RoleId = roleId;
        RelatedIds = relatedIds;
    }
}

I used a pattern called Value Object to get rid of primitive types. These value objects (Id and Either) encapsulate all the validation for a Id to be considered valid. When given to RoleAssignement constructor, you then know for sure that you are handling correct values. No more tests needed for RoleAssignement, the type system already enforce your constraints !

You can then extract tests from your scenarii to test only value object construction once. Which means that even if Id is used everywhere through the codebase, it requires only to test once.

  • Related