Árvore de páginas

Testes de unidade tem como finalidade testar toda a entrada e a saída de seu sistema, com dados inválidos ou possíveis erros que possam ocorrer durante o processo.

A construção de um código que possibilite o teste fará com que o desenvolvedor adapte e refatore seu código constantemente ao final de cada alteração.

Dessa forma quanto mais unitário for o teste mais ele trará beneficio a sua aplicação.

Como trabalhamos com uma arquitetura N-Layer o foco deste tópico é mostrar como você pode realizar os testes em uma das camadas mais importantes de sua aplicação: a camada de domínio.

Considere a estrutura de testes apresentada no capitulo anterior: http://tdn.totvs.com/display/TNF/Testes#Testes-Cenáriodetestes

Testando seu domínio

Criando a estrutura de teste

Vamos começar criando o projeto onde estarão nossos testes unitários:

Em nosso Visual Studio: File -> New -> Project e vamos escolher a opção Class Library (.NET Core).

Para utilizar os frameworks de teste instale os seguintes pacotes via nuget:

  • Microsoft.NET.Test.Sdk
  • NSubstitute
  • Shouldly
  • Tnf.App.TestBase
  • xunit
  • xunit.runner.visualstudio

Para pacotes do TNF adicione configure seu package source do nuget para nosso feed: https://www.myget.org/F/tnf/api/v3/index.json

Testando especificações

O TNF traz o uso do pattern Specifications para criação de regras para sua entidade de domínio ou processo de negócio.

Vamos considerar a seguinte especificação para nossa entidade Person de nosso cenário de testes:

PresidentShouldHaveNameSpecification.cs
using System;
using System.Linq.Expressions;
using Tnf.Specifications;
 
namespace Tnf.Architecture.Domain.Registration.Specifications
{
    internal class PersonShouldHaveNameSpecification : Specification<Person>
    {
        public override string LocalizationSource => AppConsts.LocalizationSourceName;
        public override Enum LocalizationKey => Person.Error.PersonNameMustHaveValue;
 
        public override Expression<Func<Person, bool>> ToExpression()
        {
            return (p) => !string.IsNullOrWhiteSpace(p.Name);
        }
    }
}

Essa regra valida que a entidade Person necessariamente precisa sempre ter sua propriedade "Name" informada, caso contrario ela não é considerada válida.

Geralmente a nossa regra de especificação testada está em outro assembly, visando manter nosso domínio de negocio integro, sugerimos que você defina sua entidade, especificação e builder como internal.

Para que esse assembly fique visível dentro de seu contexto de teste adicione no arquivo AssemblyInfo o attributo InternalsVisibleTo.

Agora podemos realizar o teste isolado em cima de cada especificação criada, verificando a condição da forma verdadeira ou falsa pela passagem da regra:

SpecificationsTests.cs
public class PersonSpecificationsTests
{
	[Fact]
	public void Person_Should_Have_Name()
    {
    	// Arrange
        var spec = new PersonShouldHaveNameSpecification();

        // Act
        var isSatisfied = spec.IsSatisfiedBy(new Person("George Washington"));

        // Assert
        Assert.True(isSatisfied);
	}

    [Fact]
	public void Person_Not_Accept_Invalid_Name()
    {
		// Arrange
        var spec = new PersonShouldHaveNameSpecification();

        // Act
        var isSatisfied = spec.IsSatisfiedBy(new Person());

        // Assert
        Assert.False(isSatisfied);
	}
}

Outro exemplo para uma regra de especificação é a validação também do tamanho do campo "Name" através da constante presente na entidade de negócio contendo o seu tamanho máximo. 

O método "IsSatisfiedBy" da entidade criada valida a regra escrita retornando assim um valor boleano indicando se a condição foi satisfeita ou não.

Testando builders

No TNF para a construção de entidades de negócio são utilizados Builders, onde o objeto de domínio é composto de forma fluente e também validado através de uso de N especificações adicionadas internamente a sua estrutura.

Considere o builder a seguir para a entidade "Person" como descrito em nosso cenário de testes:

PresidentBuilder.cs
using Tnf.Architecture.CrossCutting;
using Tnf.Architecture.Domain.Registration.Specifications;
using Tnf.Builder;
using Tnf.Localization;
 
namespace Tnf.Architecture.Domain.Registration
{
    internal class PersonBuilder : Builder<Person>
    {
        public PersonBuilder() : base() { }
        public PersonBuilder(Person instance) : base(instance) { }
 
        public PersonBuilder WithId(int id)
        {
            Instance.Id = id;
            return this;
        }
 
        public PersonBuilder WithName(string name)
        {
            Instance.Name = name;
            return this;
        }
 
        public override Person Build()
        {
            base.Validate();
            return base.Build();
        }
 
        protected override void Specifications()
        {
            AddSpecification(new PersonShouldHaveNameSpecification());
        }
    }
}

O método sobrescrito da classe builder realiza a chamada das especificações para a entidade validando cada aspecto descrito.

 

PresidentBuilderTests.cs
public class PersonBuilderTests : TnfAppIntegratedTestBase<DomainModuleTest>
{
	[Fact]
    public void Create_Valid_Person()
    {
		// Arrange
        var builder = new PersonBuilder(LocalNotification)
        	.WithId("1")
            .WithName("George Washington");

        // Act
        var build = builder.Build();

	    // Assert
        Assert.False(LocalNotification.HasNotification());
	}
	
	[Fact]
	public void Create_Person_With_Invalid_Name()
	{
		// Arrange
		var builder = new PersonBuilder(LocalNotification)
			.WithId("1")
            .WithName(null);

		// Act
        var build = builder.Build();

		// Assert
        Assert.True(LocalNotification.HasNotification());
        Assert.True(LocalNotification.GetAll().Any(a => a.Message == Person.Error.PersonNameMustHaveValue.ToString()));
	}
}

Note que estamos testando também a estrutura de specifications junto novamente, pois o builder faz o uso internamente do pattern.

A classe de testes do Builder herda de Tnf.App.TestBase.TnfAppIntegratedTestBase<TStartupModule> para que ele tenha acesso ao campo LocalNotification pois nesse cenário de teste ele se comportará como um singleton.

Testando serviços de domínio

Neste teste agregamos os outros dois testes já realizados: builders e specifications.

Vamos direcionar nossos testes em nossos serviços de domínio fazendo mock de nosso repositório de dados.

Abaixo temos um exemplo do mock do repositório sendo testado para a entidade de pessoa:

WhiteHouseServiceTests
public class PersonServiceTests : TestBaseWithLocalIocManager
{
	[Fact]
	public async Task Person_Service_Insert_Valid_Persons()
    {
		// Arrange
		var repository = Substitute.For<IPersonRepository>();
		var parameter = new PersonDto("1", "George Washington");
 
        repository.InsertPersonAsync(parameter).Returns(Task.FromResult(parameter));

		// Register event in test EventBus        
		LocalEventBus.Register<PersonCreatedEvent>(
        	eventData =>
            {
            	var Person = eventData.Person;
                Assert.True(Person.Id == "1");
			});

		var service = new PersonService(repository, LocalEventBus);

		// Act
		var responseBase = await service.InsertPersonAsync(parameter);

        // Assert
        Assert.False(LocalNotification.HasNotification());
	}
}

Para isso o TNF fornece uma classe abstrata chamada Tnf.App.TestBase.TestBaseWithLocalIocManager presente no pacote Tnf.App.TestBase.

Essa classe prove acesso a infraestrutura do objeto IocManager (LocalIocManager), EventBus (LocalEventBus) e NotificationHandler (LocalNotification), podendo ser usados para os registros de dependências para realização dos testes unitários.

O cenário acima descreve o teste de uma inserção válida disparando um evento de criação da entidade de Person.

Note que neste teste entra o framework que realiza o mock (NSubstitute) do método InsertPersonAsync de nosso repositório.

O teste engloba o caso de inserção e disparo do evento após a sua criação.

  • Sem rótulos