¿Debo probar los métodos heredados?

30

Supongamos que tengo un Administrador de clase derivado de un Empleado de clase base , y ese Empleado tiene un método getEmail () que hereda el Administrador . ¿Debo probar que el comportamiento del método getEmail () de un gerente es, de hecho, el mismo que el de un empleado?

En el momento en que se escriban estas pruebas, el comportamiento será el mismo, pero, por supuesto, en algún momento en el futuro alguien podría anular este método, cambiar su comportamiento y, por lo tanto, interrumpir mi aplicación. Sin embargo, parece un poco extraño probar esencialmente la ausencia de código de intromisión.

(Tenga en cuenta que probar el método Manager :: getEmail () no mejora la cobertura del código (o incluso cualquier otra métrica de calidad del código (?)) Hasta que Manager :: getEmail () se cree / anule) .

(Si la respuesta es "Sí", sería útil cierta información sobre cómo administrar las pruebas que se comparten entre las clases base y derivada).

Una formulación equivalente de la pregunta:

Si una clase derivada hereda un método de una clase base, ¿cómo expresa (prueba) si espera que el método heredado:

  1. Comportarse exactamente de la misma manera que la base en este momento (si el comportamiento de la base cambia, el comportamiento del método derivado no lo hace);
  2. Comportarse exactamente de la misma manera que la base todo el tiempo (si el comportamiento de la clase base cambia, el comportamiento de la clase derivada también cambia); o
  3. Compórtate como quiera (no te importa el comportamiento de este método porque nunca lo llamas).
mjs
fuente
1
La OMI derivando una Managerclase Employeefue el primer gran error.
CodesInChaos
44
@CodesInChaos Sí, eso podría ser un mal ejemplo. Pero el mismo problema se aplica siempre que tenga herencia.
mjs
1
Ni siquiera necesita anular el método. El método de la clase base puede llamar a otros métodos de instancia que se anulan, y ejecutar el mismo código fuente para el método aún crea un comportamiento diferente.
gnasher729

Respuestas:

23

Tomaría el enfoque pragmático aquí: si alguien, en el futuro, anula a Manager :: getMail, entonces es responsabilidad del desarrollador proporcionar el código de prueba para el nuevo método.

Por supuesto, eso solo es válido si Manager::getEmailrealmente tiene la misma ruta de código que Employee::getEmail! Incluso si el método no se anula, podría comportarse de manera diferente:

  • Employee::getEmailpodría llamar a algunos virtuales protegidos getInternalEmailque se anulan Manager.
  • Employee::getEmailpodría acceder a algún estado interno (por ejemplo, algún campo _email), que puede diferir en las dos implementaciones: por ejemplo, la implementación predeterminada Employeepodría garantizar que _emailsiempre sea así [email protected], pero Manageres más flexible en la asignación de direcciones de correo.

En tales casos, es posible que un error se manifieste solo Manager::getEmailaunque la implementación del método en sí sea ​​la misma. En ese caso, las pruebas por Manager::getEmailseparado podrían tener sentido.

Heinzi
fuente
14

Me gustaría.

Si está pensando "bueno, en realidad solo está llamando a Employee :: getEmail () porque no lo anulo, así que no necesito probar Manager :: getEmail ()", entonces no está realmente probando el comportamiento del administrador :: getEmail ().

Pensaría en lo que solo Manager :: getEmail () debería hacer, no si es heredado o anulado. Si el comportamiento de Manager :: getEmail () debe ser devolver lo que Employee :: getMail () devuelve, entonces esa es la prueba. Si el comportamiento es devolver "[email protected]", entonces esa es la prueba. No importa si se implementa por herencia o si se anula.

Lo que importa es que, si cambia en el futuro, su prueba lo detecta y usted sabe que algo se rompió o necesita ser reconsiderado.

Algunas personas pueden estar en desacuerdo con la aparente redundancia en el cheque, pero mi respuesta a eso sería que está probando el comportamiento Employee :: getMail () y Manager :: getMail () como métodos distintos, ya sea que se hereden o no. anulado Si un futuro desarrollador necesita cambiar el comportamiento de Manager :: getMail (), también debe actualizar las pruebas.

Sin embargo, las opiniones pueden variar, creo que Digger y Heinzi dieron justificaciones razonables para lo contrario.

seggy
fuente
1
Me gusta el argumento de que las pruebas de comportamiento son ortogonales a la forma en que se construye la clase. Sin embargo, parece un poco divertido ignorar por completo la herencia. (¿Y cómo gestionar las pruebas compartidas, si tiene mucha herencia?)
mjs
1
Así poner. El hecho de que Manager esté usando un método heredado, de ninguna manera significa que no debe probarse. Una gran ventaja mía dijo una vez: "Si no se prueba, se rompe". Con ese fin, si realiza las pruebas requeridas para el Empleado y el Administrador en el check-in de código, estará seguro de que el desarrollador que ingrese un nuevo código para el Administrador que pueda alterar el comportamiento del método heredado arreglará la prueba para reflejar el nuevo comportamiento . O ven a llamar a tu puerta.
hurricaneMitch
2
Realmente quieres probar la clase Manager. El hecho de que sea una subclase de Employee, y que getMail () se implemente al depender de la herencia, es solo un detalle de implementación que debe ignorar al crear pruebas unitarias. El próximo mes descubrirá que heredar Manager de Employee fue una mala idea y reemplazará toda la estructura de herencia y volverá a implementar todos los métodos. Las pruebas unitarias deben continuar probando su código sin problemas.
gnasher729
5

No lo pruebes por la unidad. Hacer funcional / aceptación probarlo.

Las pruebas unitarias deben probar cada implementación, si no está proporcionando una nueva implementación, entonces cumpla con el principio DRY. Si desea gastar algo de esfuerzo aquí, puede mejorar la prueba de la unidad original. Solo si anula el método debe escribir una prueba unitaria.

Al mismo tiempo, las pruebas funcionales / de aceptación deben garantizar que, al final del día, todo su código haga lo que se supone que debe hacer y, con suerte, detectará cualquier rareza de la herencia.

Señor Fox
fuente
4

Las reglas de Robert Martin para TDD son:

  1. No está permitido escribir ningún código de producción a menos que sea para aprobar una prueba de unidad que falla.
  2. No se le permite escribir más de una prueba unitaria de la que es suficiente para fallar; y las fallas de compilación son fallas.
  3. No se le permite escribir más código de producción del que sea suficiente para pasar la prueba de la unidad que falla.

Si sigue estas reglas, no podrá ponerse en posición de hacer esta pregunta. Si el getEmailmétodo necesita ser probado, habría sido probado.

Daniel Kaplan
fuente
3

Sí, debe probar los métodos heredados porque en el futuro pueden anularse. Además, los métodos heredados pueden llamar a métodos virtuales que se anulan , lo que cambiaría el comportamiento del método heredado no anulado.

La forma en que pruebo esto es creando una clase abstracta para probar la clase base (posiblemente abstracta) o la interfaz, de esta manera (en 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));
    }
}

Luego, tengo una clase con pruebas específicas para el Manager, e integro las pruebas de los empleados de esta manera:

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

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

La razón por la que puedo hacerlo es el principio de sustitución de Liskov: las pruebas de Employeetodavía deben pasar cuando se pasa un Managerobjeto, ya que se deriva de Employee. Así que tengo que escribir mis pruebas solo una vez, y puedo verificar que funcionen para todas las implementaciones posibles de la interfaz o clase base.

Daniel AA Pelsmaeker
fuente
Buen punto sobre el principio de sustitución de Liskov, tiene razón en que si esto se cumple, la clase derivada pasará todas las pruebas de la clase base. Sin embargo, LSP es frecuentemente violado, ¡incluso por el setUp()método de xUnit ! Y casi todos los marcos web MVC que implican anular un método de "índice" también rompen el LSP, que es básicamente todos ellos.
mjs
Esta es absolutamente la respuesta correcta: he escuchado que se llama "Patrón de prueba abstracto". Por curiosidad, ¿por qué usar una clase anidada en lugar de simplemente mezclar las pruebas a través de class ManagerTests : ExployeeTests? (es decir, Reflejando la herencia de las clases bajo prueba.) ¿Es principalmente estética, ayudar a ver los resultados?
Luke Usherwood
2

¿Debo probar que el comportamiento del método getEmail () de un gerente es, de hecho, el mismo que el de un empleado?

Yo diría que no, ya que en mi opinión sería una prueba repetida que probaría una vez en las pruebas de Empleado y eso sería todo.

En el momento en que se escriban estas pruebas, el comportamiento será el mismo, pero, por supuesto, en algún momento en el futuro alguien podría anular este método, cambiar su comportamiento

Si se anula el método, necesitaría nuevas pruebas para verificar el comportamiento anulado. Ese es el trabajo de la persona que implementa el getEmail()método de anulación .


fuente
1

No, no necesita probar métodos heredados. Las clases y sus casos de prueba que dependen de este método se interrumpirán de todos modos si el comportamiento cambia Manager.

Piense en el siguiente escenario: la dirección de correo electrónico se ensambla como [email protected]:

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

Usted ha probado esto y funciona bien para usted Employee. También has creado una clase Manager:

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

Ahora tiene una clase ManagerSortque clasifica a los administradores en una lista basada en su dirección de correo electrónico. Por supuesto, supones que la generación de correo electrónico es la misma que en Employee:

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

Escribes una prueba para tu ManagerSort:

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

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

Todo funciona bien Ahora viene alguien y anula el getEmail()método:

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

Ahora que pasa? Tu testManagerSort()fallará porque el getEmail()de Managerfue anulado. Investigará en este problema y encontrará la causa. Y todo sin escribir un caso de prueba separado para el método heredado.

Por lo tanto, no necesita probar métodos heredados.

De lo contrario, por ejemplo, en Java, tendría que probar todos los métodos heredados de Objectlike toString(), equals()etc. en cada clase.

Uooo
fuente
No se supone que seas inteligente con las pruebas unitarias. Dices "No necesito probar X porque cuando X falla, Y falla". Pero las pruebas unitarias se basan en los supuestos de errores en su código. Con errores en su código, ¿por qué pensaría que las relaciones complejas entre pruebas funcionan de la manera que espera que funcionen?
gnasher729
@ gnasher729 Digo "No necesito probar X en la subclase porque ya está probado en las pruebas unitarias para la superclase ". Por supuesto, si cambia la implementación de X, debe escribir las pruebas apropiadas para ello.
Uooo