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
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:
Para pacotes do TNF adicione configure seu package source do nuget para nosso feed: https://www.myget.org/F/tnf/api/v3/index.json
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:
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:
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.
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:
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.
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.
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:
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.