Database
 sql >> Base de Dados >  >> RDS >> Database

Por que usar testes unitários é um ótimo investimento em arquitetura de alta qualidade


Decidi escrever este artigo para mostrar que os testes de unidade não são apenas uma ferramenta para lidar com a regressão no código, mas também um grande investimento em uma arquitetura de alta qualidade. Além disso, um tópico na comunidade .NET em inglês me motivou a fazer isso. O autor do artigo foi Johnnie. Ele descreveu seu primeiro e último dia na empresa envolvida no desenvolvimento de software para negócios no setor financeiro. Johnnie estava se candidatando ao cargo – de desenvolvedor de testes unitários. Ele estava chateado com a má qualidade do código, que ele teve que testar. Ele comparou o código com um ferro-velho cheio de objetos que clonam uns aos outros em qualquer lugar inadequado. Além disso, ele não conseguia encontrar tipos de dados abstratos em um repositório:o código continha apenas vinculação de implementações que se requisitavam.



Johnnie, percebendo toda a inutilidade dos testes de módulo nesta empresa, descreveu essa situação para o gerente, recusou-se a cooperar mais e deu um conselho valioso. Ele recomendou que uma equipe de desenvolvimento fizesse cursos para aprender a instanciar objetos e usar tipos de dados abstratos. Não sei se o gerente seguiu seu conselho (acho que não). No entanto, se você estiver interessado no que Johnnie quis dizer e como o uso de testes de módulo pode influenciar a qualidade de sua arquitetura, leia este artigo.

O isolamento de dependência é uma base de teste de módulo


Módulo ou teste de unidade é um teste que verifica a funcionalidade do módulo isolada de suas dependências. O isolamento de dependência é uma substituição de objetos do mundo real, com os quais o módulo que está sendo testado interage, por stubs que simulam o comportamento correto de seus protótipos. Essa substituição permite focar em testar um determinado módulo, ignorando um possível comportamento incorreto de seu ambiente. A necessidade de substituir dependências no teste causa uma propriedade interessante. Um desenvolvedor que percebe que seu código será usado em testes de módulo deve desenvolver usando abstrações e realizar refatoração nos primeiros sinais de alta conectividade.

Vou considerá-lo no exemplo particular.

Vamos tentar imaginar como seria um módulo de mensagem pessoal em um sistema desenvolvido pela empresa da qual Johnnie escapou. E como seria o mesmo módulo se os desenvolvedores aplicassem testes de unidade.

O módulo deve ser capaz de armazenar a mensagem no banco de dados e se a pessoa a quem a mensagem foi endereçada estiver no sistema — exibir a mensagem na tela com uma notificação de brinde.
//A module for sending messages in C#. Version 1.
public class MessagingService
{
    public void SendMessage(Guid messageAuthorId, Guid messageRecieverId, string message)
    {
        //A repository object stores a message in a database
        new MessagesRepository().SaveMessage(messageAuthorId, messageRecieverId, message);
        //check if the user is online  
        if (UsersService.IsUserOnline(messageRecieverId))
        {
            //send a toast notification calling the method of a static object  
            NotificationsService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
        }
    }
}

Vamos verificar quais dependências nosso módulo possui.

A função SendMessage invoca métodos estáticos dos objetos Notificationsservice e Usersservice e cria o objeto Messagesrepository que é responsável por trabalhar com o banco de dados.

Não há problema com o fato do módulo interagir com outros objetos. O problema é como essa interação é construída e não é construída com sucesso. O acesso direto a métodos de terceiros tornou nosso módulo fortemente vinculado a implementações específicas.

Essa interação tem muitas desvantagens, mas o importante é que o módulo Messagingservice perdeu a capacidade de ser testado isoladamente das implementações do Notificationsservice, Usersservice e Messagesrepository. Na verdade, não podemos substituir esses objetos por stubs.

Agora vamos ver como o mesmo módulo ficaria se um desenvolvedor cuidasse dele.
//A module for sending messages in C#. Version  2.
public class MessagingService: IMessagingService
{
    private readonly IUserService _userService;
    private readonly INotificationService _notificationService;
    private readonly IMessagesRepository _messagesRepository;

    public MessagingService(IUserService userService, INotificationService notificationService, IMessagesRepository messagesRepository)
    {
        _userService = userService;
        _notificationService = notificationService;
        _messagesRepository = messagesRepository;
    }

    public void AddMessage(Guid messageAuthorId, Guid messageRecieverId, string message)
    {
        //A repository object stores a message in a database.  
        _messagesRepository.SaveMessage(messageAuthorId, messageRecieverId, message);
        //check if the user is online  
        if (_userService.IsUserOnline(messageRecieverId))
        {
            //send a toast message
            _notificationService.SendNotificationToUser(messageAuthorId, messageRecieverId, message);
        }
    }
}

Como você pode ver, esta versão é muito melhor. A interação entre objetos agora é construída não diretamente, mas por meio de interfaces.

Não precisamos mais acessar classes estáticas e instanciar objetos em métodos com lógica de negócios. O ponto principal é que podemos substituir todas as dependências passando stubs para teste em um construtor. Assim, enquanto aprimoramos a testabilidade do código, também podemos melhorar a testabilidade do nosso código e a arquitetura do nosso aplicativo. Nós nos recusamos a usar implementações diretas e passamos a instanciação para a camada acima. Isso é exatamente o que Johnnie queria.

Em seguida, crie um teste para o módulo de envio de mensagens.

Especificação em testes

Defina o que nosso teste deve verificar:
  • Uma única chamada do método SaveMessage
  • Uma única chamada do método SendNotificationToUser() se o stub do método IsUserOnline() sobre o objeto IUsersService retornar true
  • Não há método SendNotificationToUser() se o stub do método IsUserOnline() sobre o objeto IUsersService retornar false

Seguir essas condições pode garantir que a implementação da mensagem SendMessage esteja correta e não contenha erros.

Testes

O teste é implementado usando o framework Moq isolado
[TestMethod]
public void AddMessage_MessageAdded_SavedOnce()
{
    //Arrange
    //sender
    Guid messageAuthorId = Guid.NewGuid();
    //receiver who is online
    Guid recieverId = Guid.NewGuid();
    //a message sent from a sender to a receiver
    string msg = "message";
    // stub for the IsUserOnline interface of the IUserService method
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(It.IsAny<Guid>())).Returns(true);
    //mocks for INotificationService and IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    //create a module for messages passing mocks and stubs as dependencies 
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);

    //Act
    messagingService.AddMessage(messageAuthorId, recieverId, msg);

    //Assert
    repositoryMoq.Verify(x => x.SaveMessage(messageAuthorId, recieverId, msg), Times.Once);
   
}

[TestMethod]
public void AddMessage_MessageSendedToOffnlineUser_NotificationDoesntRecieved()
{
    //Arrange
    //sender
    Guid messageAuthorId = Guid.NewGuid();
    //receiver who is offline
    Guid offlineReciever = Guid.NewGuid();
    //message sent from a sender to a receiver
    string msg = "message";
    // stub for the IsUserOnline interface of the IUserService method
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(offlineReciever)).Returns(false);
    //mocks for INotificationService and IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    // create a module for messages passing mocks and stubs as dependencies
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);
    //Act
    messagingService.AddMessage(messageAuthorId, offlineReciever, msg);

    //Assert
    notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, offlineReciever, msg),
                                    Times.Never);
}

[TestMethod]
public void AddMessage_MessageSendedToOnlineUser_NotificationRecieved()
{
    //Arrange
    //sender
    Guid messageAuthorId = Guid.NewGuid();
    //receiver who is online
    Guid onlineRecieverId = Guid.NewGuid();
    //message sent from a sender to a receiver 
    string msg = "message";
    // stub for the IsUserOnline interface of the IUserService method
    Mock<IUserService> userServiceStub = new Mock<IUserService>(new MockBehavior());
    userServiceStub.Setup(x => x.IsUserOnline(onlineRecieverId)).Returns(true);
    //mocks for INotificationService and IMessagesRepository
    Mock<INotificationService> notificationsServiceMoq = new Mock<INotificationService>();
    Mock<IMessagesRepository> repositoryMoq = new Mock<IMessagesRepository>();
    //create a module for messages passing mocks and stubs as dependencies
    var messagingService = new MessagingService(userServiceStub.Object, notificationsServiceMoq.Object,
                                                repositoryMoq.Object);

    //Act
    messagingService.AddMessage(messageAuthorId, onlineRecieverId, msg);

    //Assert
    notificationsServiceMoq.Verify(x => x.SendNotificationToUser(messageAuthorId, onlineRecieverId, msg),
                                    Times.Once);
}

Resumindo, buscar uma arquitetura ideal é uma tarefa inútil.

Os testes de unidade são ótimos para usar quando você precisa verificar a arquitetura em perda de acoplamento entre os módulos. Ainda assim, tenha em mente que projetar sistemas de engenharia complexos é sempre um compromisso. Não existe uma arquitetura ideal e não é possível levar em conta todos os cenários de desenvolvimento da aplicação de antemão. A qualidade da arquitetura depende de vários parâmetros, muitas vezes mutuamente exclusivos. Você pode resolver qualquer problema de design adicionando um nível adicional de abstração. No entanto, não se refere ao problema de uma enorme quantidade de níveis de abstração. Não recomendo pensar que a interação entre objetos é baseada apenas em abstrações. A questão é que você usa o código que permite a interação entre as implementações e é menos flexível, o que significa que não tem a possibilidade de ser testado por testes unitários.