Devo testar métodos herdados?

30

Suponha que eu tenha um gerente de classe derivado de uma classe base Employee e que Employee tenha um método getEmail () herdado pelo Manager . Devo testar se o comportamento do método getEmail () de um gerente é de fato o mesmo que o de um funcionário?

No momento em que esses testes são escritos, o comportamento será o mesmo, mas é claro que em algum momento no futuro alguém poderá substituir esse método, alterar seu comportamento e, portanto, interromper meu aplicativo. No entanto, parece um pouco estranho testar essencialmente a ausência de código de intromissão.

(Observe que o método Manager :: getEmail () de teste não melhora a cobertura do código (ou qualquer outra métrica de qualidade do código (?)) Até que o Manager :: getEmail () seja criado / substituído.)

(Se a resposta for "Sim", algumas informações sobre como gerenciar testes compartilhados entre as classes base e derivadas serão úteis.)

Uma formulação equivalente da pergunta:

Se uma classe derivada herda um método de uma classe base, como você expressa (teste) se espera que o método herdado:

  1. Comporte-se exatamente da mesma maneira que a base agora (se o comportamento da base mudar, o comportamento do método derivado não);
  2. Comporte-se exatamente da mesma maneira que a base para todos os tempos (se o comportamento da classe base mudar, o comportamento da classe derivada também mudará); ou
  3. Comporte-se como quiser (você não se importa com o comportamento desse método porque nunca o chama).
mjs
fonte
11
A OMI derivar uma Managerclasse Employeefoi o primeiro grande erro.
precisa saber é o seguinte
4
@CodesInChaos Sim, isso pode ser um mau exemplo. Mas o mesmo problema se aplica sempre que você tem herança.
MJS
11
Você nem precisa substituir o método. O método da classe base pode chamar outros métodos de instância que são substituídos e a execução do mesmo código-fonte para o método ainda cria um comportamento diferente.
8897 #

Respostas:

23

Eu adotaria a abordagem pragmática aqui: se alguém, no futuro, substituir o Manager :: getMail, será responsabilidade do desenvolvedor fornecer código de teste para o novo método.

Claro que isso só é válido se Manager::getEmailrealmente tiver o mesmo caminho de código que Employee::getEmail! Mesmo que o método não seja substituído, ele pode se comportar de maneira diferente:

  • Employee::getEmailpoderia chamar algum virtual protegido getInternalEmailque é substituído Manager.
  • Employee::getEmailpoderia acessar algum estado interno (por exemplo, algum campo _email), que pode diferir nas duas implementações: Por exemplo, a implementação padrão de Employeepoderia garantir isso _emailsempre [email protected], mas Manageré mais flexível na atribuição de endereços de email.

Nesses casos, é possível que um erro se manifeste apenas em Manager::getEmail, mesmo que a implementação do método em si seja a mesma. Nesse caso, testar Manager::getEmailseparadamente pode fazer sentido.

Heinzi
fonte
14

Eu gostaria.

Se você está pensando "bem, é realmente apenas chamando Employee :: getEmail () porque eu não o substitui, então não preciso testar o Manager :: getEmail ()", então você não está realmente testando o comportamento do Manager :: getEmail ().

Eu pensaria no que apenas o Manager :: getEmail () deve fazer, não se é herdado ou substituído. Se o comportamento de Manager :: getEmail () for retornar o que Employee :: getMail () retornar, esse é o teste. Se o comportamento é retornar "[email protected]", esse é o teste. Não importa se é implementado por herança ou substituído.

O que importa é que, se ele mudar no futuro, seu teste o capturará e você saberá que algo foi quebrado ou precisa ser reconsiderado.

Algumas pessoas podem discordar da aparente redundância na verificação, mas o meu contrário é que você está testando o comportamento Employee :: getMail () e Manager :: getMail () como métodos distintos, sejam eles herdados ou não ou substituído. Se um desenvolvedor futuro precisar alterar o comportamento do Manager :: getMail (), também precisará atualizar os testes.

As opiniões podem variar, acho que Digger e Heinzi deram justificativas razoáveis ​​para o contrário.

seggy
fonte
11
Gosto do argumento de que o teste comportamental é ortogonal ao modo como a classe é construída. Parece um pouco engraçado ignorar completamente a herança. (E como você a gerenciar os testes compartilhados, se você tem um monte de herança?)
MJS
11
Bem colocado. Só porque o Manager está usando um método herdado, de forma alguma significa que ele não deve ser testado. Um grande líder meu disse uma vez: "Se não for testado, está quebrado". Para esse fim, se você fizer os testes para Funcionário e Gerente exigidos no check-in de código, terá certeza de que o desenvolvedor que estiver verificando o novo código do Manager que pode alterar o comportamento do método herdado corrigirá o teste para refletir o novo comportamento . Ou venha bater à sua porta.
Furacão Mitch
2
Você realmente deseja testar a classe Manager. O fato de ser uma subclasse de Employee e de getMail () ser implementado com base na herança, é apenas um detalhe de implementação que você deve ignorar ao criar testes de unidade. No mês que vem, você descobre que herdar o Manager do Employee era uma má ideia e substitui toda a estrutura de herança e reimplementou todos os métodos. Seus testes de unidade devem continuar testando seu código sem problemas.
8897 #
5

Não teste a unidade. Teste funcional / de aceitação.

Os testes de unidade devem testar todas as implementações, se você não estiver fornecendo uma nova implementação, siga o princípio DRY. Se você quiser gastar algum esforço aqui, pode aprimorar o teste de unidade original. Somente se você substituir o método, deverá escrever um teste de unidade.

Ao mesmo tempo, os testes funcionais / de aceitação devem garantir que, no final do dia, todo o seu código faça o que é suposto e, esperançosamente, capte qualquer estranheza da herança.

MrFox
fonte
4

As regras de Robert Martin para TDD são:

  1. Você não tem permissão para escrever nenhum código de produção, a menos que seja aprovado no teste de unidade.
  2. Você não tem permissão para escrever mais testes de unidade que o suficiente para falhar; e falhas de compilação são falhas.
  3. Você não tem permissão para escrever mais código de produção que o suficiente para passar no único teste de unidade com falha.

Se você seguir essas regras, não poderá entrar em uma posição para fazer essa pergunta. Se o getEmailmétodo precisar ser testado, ele teria sido testado.

Daniel Kaplan
fonte
3

Sim, você deve testar os métodos herdados, pois no futuro eles poderão ser substituídos. Além disso, os métodos herdados podem chamar métodos virtuais substituídos , o que alteraria o comportamento do método herdado não substituído.

A maneira como eu testo isso é criando uma classe abstrata para testar a classe base ou a interface (possivelmente abstrata), desta forma (em C # usando NUnit):

public abstract class EmployeeTests
{
    protected abstract Employee CreateInstance(string name, int age);

    [Test]
    public void GetEmail_ReturnsValidEmailAddress()
    {
        // Given
        var sut = CreateInstance("John Doe", 20);

        // When
        string email = sut.GetEmail();

        // Then
        Assert.IsTrue(Helper.IsValidEmail(email));
    }
}

Então, eu tenho uma classe com testes específicos para o Managere integro os testes do funcionário como este:

[TestFixture]
public class ManagerTests
{
    // Other tests.

    [TestFixture]
    public class ManagerEmployeeTests : EmployeeTests
    {
        protected override Employee CreateInstance(string name, int age);
        {
            return new Manager(name, age);
        }
    }
}

A razão pela qual posso fazê-lo é o princípio de substituição de Liskov: os testes de Employeeainda devem passar quando um Managerobjeto é passado, uma vez que deriva dele Employee. Portanto, preciso escrever meus testes apenas uma vez e verificar se eles funcionam para todas as implementações possíveis da interface ou da classe base.

Daniel AA Pelsmaeker
fonte
Bom argumento quanto ao princípio de substituição de Liskov, você está certo de que, se isso acontecer, a classe derivada passará em todos os testes da classe base. No entanto, o LSP é frequentemente violado, inclusive pelo setUp()próprio método do xUnit ! E praticamente toda estrutura de MVC da Web que envolve a substituição de um método "index" também quebra o LSP, que é basicamente todos eles.
MJS
Esta é absolutamente a resposta correta - já ouvi falar em "Abstract Test Pattern". Por curiosidade, por que usar uma classe aninhada em vez de apenas misturar os testes via class ManagerTests : ExployeeTests? (isto é, espelhando a herança das classes em teste.) É principalmente estética, para ajudar na visualização dos resultados?
Luke Usherwood
2

Devo testar se o comportamento do método getEmail () de um gerente é de fato o mesmo que o de um funcionário?

Eu diria que não, pois, na minha opinião, seria um teste repetido que eu testaria uma vez nos testes com funcionários e seria isso.

No momento em que esses testes são escritos, o comportamento será o mesmo, mas é claro que em algum momento no futuro alguém poderá substituir esse método, alterar seu comportamento

Se o método for substituído, você precisará de novos testes para verificar o comportamento substituído. Esse é o trabalho da pessoa que implementa o getEmail()método de substituição .


fonte
1

Não, você não precisa testar métodos herdados. As classes e seus casos de teste que dependem desse método serão interrompidos de qualquer maneira se o comportamento for alterado Manager.

Pense no seguinte cenário: O endereço de email é montado como [email protected]:

class Employee{
    String firstname, lastname;
    String getEmail() { 
        return firstname + "." + lastname + "@example.com";
    }
}

Você testou a unidade e funciona bem para o seu Employee. Você também criou uma classe Manager:

class Manager extends Employee { /* nothing different in email generation */ }

Agora você tem uma classe ManagerSortque classifica os gerentes em uma lista com base no endereço de email. De sua parte, você supõe que a geração de e-mail seja a mesma que em Employee:

class ManagerSort {
    void sortManagers(Manager[] managerArrayToBeSorted)
        // sort based on email address omitted
    }
}

Você escreve um teste para o seu ManagerSort:

void testManagerSort() {
    Manager[] managers = ... // build random manager list
    ManagerSort.sortManagers(managers);

    Manager[] expected = ... // expected result
    assertEquals(expected, managers); // check the result
}

Tudo funciona bem. Agora alguém vem e substitui o getEmail()método:

class Manager extends Employee {
    String getEmail(){
        // managers should have their lastname and firstname order changed
        return lastname + "." + firstname + "@example.com";
    }
}

Agora o que acontece? Sua testManagerSort()falha falhará porque o getEmail()de Managerfoi substituído. Você investigará esse problema e encontrará a causa. E tudo sem escrever uma caixa de teste separada para o método herdado.

Portanto, você não precisa testar métodos herdados.

Caso contrário, por exemplo, em Java, você teria que testar todos os métodos herdados de Objectlike toString(), equals()etc. em todas as classes.

Uooo
fonte
Você não deve ser inteligente com testes de unidade. Você diz "Não preciso testar o X porque, quando X falha, Y falha". Mas os testes de unidade são baseados nas suposições de bugs no seu código. Com bugs no seu código, por que você acha que relações complexas entre testes funcionam da maneira que você espera que funcionem?
Gnasher729
@ gnasher729 Eu digo "Não preciso testar o X na subclasse porque ele já foi testado nos testes de unidade da superclasse ". Obviamente, se você alterar a implementação do X, precisará escrever testes apropriados.
Uooo