Unit Testing – API Architecture for general purpose

  • by

In the first post, we discussed the API architecture from a developers perspective and what is required for a developer to utilize the architecture. We looked at how the SOLID principles applied to the architecture, allowing us to write decoupled units of code and isolating our business logic in smaller well-structured classes.

In this post, we will look at how to apply unit testing by taking advantage of the modularity and isolation of these handlers as well as how to utilize the IOC container for testing.

Unit Testing

Unit tests are short, quick, and automated tests that make sure a specific part of your program works. They test specific functionality of a method or class that has a clear pass/fail condition. By writing unit tests, developers can make sure their code works, before passing it to QA for further testing.[1]

Authorization Failure Test

[TestClass]
public class AuthorizationUnitTest_Fail : UnitTestBase
{
	private int _leadId;
	private GetLeadDto _getLeadDto;
	private IAuthorizationContext<LeadDto> _getResponse;

	public AuthorizationUnitTest_Fail()
	{
	}

	[TestMethod]
	public void GetLeadDtoAuthorization()
	{
		base.RegisterModule(
			new Module_ApplicationUserContext_With_No_Authorization());

		base.Run(
			"GetLeadDto Authorization - FAIL",
			"Given a user with no Authorization",
			"When Authorizing the request",
			"Then an error message was received");
	}

	public override void Seed()
	{
		_leadId = 0;
	}

	protected override void Given()
	{
		_getLeadDto = new GetLeadDto() { Id = _leadId };
	}

	protected override void When()
	{
		_getResponse = ApplicationContext
			.Container
			.Resolve<IHandler<GetLeadDto, IAuthorizationContext<LeadDto>>>()
			.Handle(_getLeadDto);
	}

	protected override void Then()
	{
		Assert.IsTrue(_getResponse.Messages.Any(a =>
			a.Type == Error &&
			a.Key == "E3"));
	}
}

In the test class above we test the GetLeadDtoAuthorizationHandler mentioned in the developers perspective post. The handler checks if the user making the GetLeadDto request is authorized to execute the request / GetLeadDtoWorkerHandler. The handler requires the user to have two authorization groups, Group A and Group B, to allow the request to continue.

base.Run(
	"GetLeadDto Authorization - FAIL",
	"Given a user with no Authorization",
	"When Authorizing the request",
	"Then an error message was received");

The purpose of the test above is described by the parameters of the Run method on the base class. These parameters are used to generate documentation when the tests are executed. When investigating a failing test, the purpose of the test is clear saving us some time with the investigation. A test with an unclear objective tends to be long, difficult to understand and often tests more than one thing.

The run method facilitates the four methods; Seed, Given, When and Then. These methods contribute to the objective and understanding of the test by confining responsibility to smaller methods.

A test should be deterministic, repeatable and produce consistent results. A test that fails some of the time is as good as no test. Handlers are tested in isolation by managing dependencies through the IOC container. For each test starting up, a new IOC container is created and shared dependencies are registered with the IOC container in the base class. These dependencies, as well as handler specific dependencies, can be stubbed and replaced specifically for testing in the test method. This allows us to fake, stub or mock dependencies like network interaction that can make your test fragile.

base.RegisterModule(
	new Module_ApplicationUserContext_With_No_Authorization());

In our test, we want to inject an Application User Context that does not contain any authentication groups. The authentication handler we are testing requires a user with Group A and Group B access. We do this by calling the RegisterModule method in the base class. By default, a user with both groups A and B is injected, because we register a new module (Module_ApplicationUserContext_With_No_Authorization) the default user is overwritten for this test.

_getResponse = ApplicationContext
	.Container
	.Resolve<IHandler<GetLeadDto, IAuthorizationContext<LeadDto>>>()
	.Handle(_getLeadDto);

The handler we are testing is resolved from the IOC container and executed in the When method. We assign the result to a private variable _getResponse making it accessible to the Then method for assertions. This forces us to focus on the result of the handler and not the implementation of the handler. When the implementation of the handler changes the test is less likely to fail, making the test less fragile. Finally, we have a single assertion to check the response from the handler. Using this test class anatomy, each of the different types of handlers (authorization, validation and worker handlers) can be tested in isolation.

  1. https://esj.com/articles/2012/09/24/better-unit-testing.aspx (2019/03/11)
  2. https://stackify.com/unit-testing-basics-best-practices (2019/03/11)