it-swarm-es.com

¿Está bien tener múltiples afirmaciones en una sola prueba de unidad?

En el comentario a esta gran publicación , Roy Osherove mencionó el proyecto OAPT que está diseñado para ejecutar cada afirmación en una sola prueba.

Lo siguiente está escrito en la página de inicio del proyecto:

Las pruebas unitarias adecuadas deberían fallar exactamente por una razón, es por eso que debería usar una afirmación por prueba unitaria.

Y, también, Roy escribió en comentarios:

Mi pauta generalmente es que pruebe un CONCEPTO lógico por prueba. puedes tener múltiples afirmaciones en el mismo ¡objeto. generalmente serán el mismo concepto que se está probando.

Creo que hay algunos casos en los que se necesitan múltiples afirmaciones (por ejemplo, Afirmación de guardia ), pero en general trato de evitar esto. ¿Cuál es tu opinión? Proporcione un ejemplo del mundo real donde múltiples afirmaciones son realmente ¡necesarias.

430
Restuta

No creo que sea necesariamente algo malo , pero sí creo que deberíamos esforzarnos por tener solo afirmaciones en nuestras pruebas. Esto significa que usted escribe muchas más pruebas y nuestras pruebas terminarían probando solo una cosa a la vez.

Dicho esto, diría que tal vez la mitad de mis pruebas solo tienen una afirmación. Creo que solo se convierte en un código (¿prueba?) Olor cuando tienes alrededor de cinco o más afirmaciones en tu prueba.

¿Cómo resuelve múltiples afirmaciones?

246
Jaco Pretorius

Las pruebas deben fallar solo por una razón, pero eso no siempre significa que solo debe haber una declaración Assert. En mi humilde opinión, es más importante mantener el patrón " Organizar, Actuar, Afirmar ".

La clave es que solo tiene una acción, y luego inspecciona los resultados de esa acción utilizando afirmaciones. Pero es "Organizar, Actuar, Afirmar, ¡Fin de la prueba". Si tiene la tentación de continuar con las pruebas realizando otra acción y más afirmaciones después, realice una prueba por separado.

Estoy feliz de ver múltiples declaraciones de afirmación que forman parte de probar la misma acción. p.ej.

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

o

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Usted podría combinarlos en una afirmación, pero eso es diferente de insistir en que debería o debe. No hay mejora al combinarlos.

p.ej. El primero podría ser

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Pero esto no es mejor: el mensaje de error es menos específico y no tiene otras ventajas. Estoy seguro de que puede pensar en otros ejemplos en los que combinar dos o tres (o más) afirmaciones en una gran condición booleana hace que sea más difícil de leer, más difícil de alterar y más difícil averiguar por qué falló. ¿Por qué hacer esto solo por el bien de una regla?

[~ # ~] nb [~ # ~] : El código que estoy escribiendo aquí es C # con NUnit, pero los principios se mantendrán con otros idiomas y marcos. La sintaxis también puede ser muy similar.

314
Anthony

Nunca pensé que más de una afirmación fuera algo malo.

Lo hago todo el tiempo:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Aquí uso múltiples afirmaciones para asegurarme de que las condiciones complejas se puedan convertir en el predicado esperado.

Solo estoy probando una unidad (el método ToPredicate), pero estoy cubriendo todo lo que puedo pensar en la prueba.

85
Matt Ellen

Cuando uso pruebas unitarias para validar el comportamiento de alto nivel, pongo múltiples afirmaciones en una sola prueba. Aquí hay una prueba que estoy usando para algún código de notificación de emergencia. El código que se ejecuta antes de la prueba coloca el sistema en un estado en el que si se ejecuta el procesador principal, se envía una alarma.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Representa las condiciones que deben existir en cada paso del proceso para que tenga la certeza de que el código se está comportando de la manera que espero. Si falla una sola afirmación, no me importa que las restantes ni siquiera se ejecuten; debido a que el estado del sistema ya no es válido, esas afirmaciones posteriores no me dirían nada valioso. * Si assertAllUnitsAlerting() falló, entonces no sabría qué hacer con assertAllNotificationSent() 's éxito OR fracaso hasta que determiné qué estaba causando el error anterior y lo corregí.

(* - Bien, posiblemente podrían ser útiles para depurar el problema. Pero la información más importante, que la prueba falló, ya se ha recibido).

21
BlairHippo

Otra razón por la que creo que múltiples afirmaciones en un método no es algo malo se describe en el siguiente código:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

En mi prueba, simplemente quiero probar que service.process() devuelve el número correcto en Inner instancias de clase.

En lugar de probar ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Estoy haciendo

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
8
Betlista

Creo que hay muchos casos en los que escribir afirmaciones múltiples es válido dentro de la regla de que una prueba solo debe fallar por una razón.

Por ejemplo, imagine una función que analiza una cadena de fecha:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Si la prueba falla, es por una razón, el análisis es incorrecto. Si usted argumenta que esta prueba puede fallar por tres razones diferentes, en mi humilde opinión sería demasiado fino en su definición de "una razón".

6
Pete

No conozco ninguna situación en la que sería una buena idea tener múltiples afirmaciones dentro del método [Prueba] en sí. La razón principal por la que a las personas les gusta tener múltiples aserciones es porque están tratando de tener una clase [TestFixture] para cada clase que se está probando. En cambio, puede dividir sus pruebas en más clases [TestFixture]. Esto le permite ver varias formas en las que el código puede no haber reaccionado de la manera que esperaba, en lugar de solo aquella en la que falló la primera aserción. La forma de lograr esto es que tiene al menos un directorio por clase siendo probado con muchas clases [TestFixture] dentro. Cada clase [TestFixture] recibiría el nombre del estado específico de un objeto que se probará. El método [SetUp] llevará el objeto al estado descrito por el nombre de la clase. Luego tiene varios métodos [Test], cada uno afirmando cosas diferentes que esperaría que fueran verdaderas, dado el estado actual del objeto. Cada método [Test] lleva el nombre de lo que está afirmando, excepto que tal vez podría llamarse después del concepto en lugar de solo una lectura en inglés del código. Entonces, la implementación de cada método [Test] solo necesita una sola línea de código donde se afirma algo. Otra ventaja de este enfoque es que hace que las pruebas sean muy legibles ya que queda bastante claro lo que está probando y lo que espera con solo mirar los nombres de clase y método. Esto también se escalará mejor a medida que comience a darse cuenta de todos los pequeños casos de Edge que desea probar y a medida que encuentre errores.

Por lo general, esto significa que la línea final de código dentro del método [SetUp] debe almacenar un valor de propiedad o un valor de retorno en una variable de instancia privada de [TestFixture]. Entonces puede afirmar varias cosas diferentes sobre esta variable de instancia a partir de diferentes métodos [Prueba]. También puede hacer afirmaciones sobre qué diferentes propiedades del objeto bajo prueba están configuradas ahora que está en el estado deseado.

A veces es necesario hacer afirmaciones en el camino, ya que está obteniendo el objeto bajo prueba en el estado deseado para asegurarse de que no se equivocó antes de poner el objeto en el estado deseado. En ese caso, esas aserciones adicionales deberían aparecer dentro del método [SetUp]. Si algo sale mal dentro del método [Configuración], quedará claro que algo estaba mal con la prueba antes de que el objeto llegara al estado deseado que pretendía probar.

Otro problema con el que puede encontrarse es que puede estar probando una excepción que esperaba que se lanzara. Esto puede tentarlo a no seguir el modelo anterior. Sin embargo, aún se puede lograr capturando la excepción dentro del método [SetUp] y almacenándola en una variable de instancia. Esto le permitirá afirmar diferentes cosas sobre la excepción, cada una en su propio método [Test]. Luego, también puede hacer valer otras cosas sobre el objeto bajo prueba para asegurarse de que no se produjeron efectos secundarios no deseados por la excepción.

Ejemplo (esto se dividiría en varios archivos):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
3
still_dreaming_1

Si tiene múltiples afirmaciones en una sola función de prueba, espero que sean directamente relevantes para la prueba que está realizando. Por ejemplo,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Tener muchas pruebas (incluso cuando sientas que probablemente sea una exageración) no es algo malo. Puede argumentar que tener las pruebas vitales y más esenciales es más importante. Por lo tanto, cuando esté afirmando, asegúrese de que sus declaraciones de afirmación estén colocadas correctamente en lugar de preocuparse demasiado por las afirmaciones múltiples. Si necesita más de uno, use más de uno.

2
hagubear

Tener múltiples aserciones en la misma prueba es solo un problema cuando la prueba falla. Entonces es posible que deba depurar la prueba o analizar la excepción para descubrir qué afirmación es la que falla. Con una afirmación en cada prueba, generalmente es más fácil determinar qué está mal.

No puedo pensar en un escenario en el que las aserciones múltiples sean realmente necesarias, ya que siempre puedes reescribirlas como condiciones múltiples en la misma aserción. Sin embargo, puede ser preferible si, por ejemplo, tiene varios pasos para verificar los datos intermedios entre los pasos en lugar de arriesgarse a que los pasos posteriores se bloqueen debido a una entrada incorrecta.

2
Guffa

Si su prueba falla, no sabrá si las siguientes afirmaciones también se romperán. A menudo, eso significa que se perderá información valiosa para descubrir la fuente del problema. Mi solución es usar una afirmación pero con varios valores:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

Eso me permite ver todas las afirmaciones fallidas a la vez. Utilizo varias líneas porque la mayoría de los IDE mostrarán diferencias de cadena en un diálogo de comparación de lado a lado.

2
Aaron Digulla

El objetivo de la prueba unitaria es brindarle la mayor cantidad de información posible sobre lo que está fallando, pero también ayudar a identificar con precisión los problemas más fundamentales primero. Cuando sabe lógicamente que una aserción fallará dado que otra aserción falla o, en otras palabras, existe una relación de dependencia entre la prueba, entonces tiene sentido rodarlas como afirmaciones múltiples dentro de una sola prueba. Esto tiene el beneficio de no ensuciar los resultados de la prueba con fallas obvias que podrían haberse eliminado si rescatamos la primera afirmación dentro de una sola prueba. En el caso de que esta relación no exista, la preferencia sería, naturalmente, separar estas afirmaciones en pruebas individuales porque, de lo contrario, encontrar estas fallas requeriría múltiples iteraciones de ejecuciones de pruebas para resolver todos los problemas.

Si luego diseña también las unidades/clases de tal manera que tendrían que escribirse pruebas demasiado complejas, esto genera menos carga durante las pruebas y probablemente promueva un mejor diseño.

1
jpierson

Sí, está bien tener múltiples aserciones siempre y cuando una prueba fallida le brinde suficiente información para poder diagnosticar la falla. Esto dependerá de lo que esté probando y cuáles sean los modos de falla.

Las pruebas unitarias adecuadas deberían fallar exactamente por una razón, es por eso que debe usar una afirmación por prueba unitaria.

Nunca he encontrado que tales formulaciones sean útiles (que una clase debería tener una razón para cambiar es un ejemplo de un adagio tan inútil). Considere una afirmación de que dos cadenas son iguales, esto es semánticamente equivalente a afirmar que la longitud de las dos cadenas es la misma y que cada carácter en el índice correspondiente es igual.

Podríamos generalizar y decir que cualquier sistema de aserciones múltiples podría reescribirse como una sola aserción, y cualquier aserción individual podría descomponerse en un conjunto de aserciones más pequeñas.

Por lo tanto, solo concéntrese en la claridad del código y la claridad de los resultados de la prueba, y deje que eso guíe el número de afirmaciones que usa en lugar de viceversa.

1
CurtainDog

Esta pregunta está relacionada con el clásico problema de equilibrio entre los problemas de spaghetti y lasaña.

Tener múltiples afirmaciones podría fácilmente meterse en el problema de los espaguetis donde no tienes una idea de qué se trata la prueba, pero tener una sola afirmación por prueba podría hacer que tu prueba sea igualmente ilegible al tener múltiples pruebas en una gran lasaña, lo que hace imposible encontrar qué prueba hace .

Hay algunas excepciones, pero en este caso, mantener el péndulo en el medio es la respuesta.

0
gsf

La respuesta es muy simple: si prueba una función que cambia más de un atributo, del mismo objeto, o incluso dos objetos diferentes, y la corrección de la función depende de los resultados de todos esos cambios, entonces desea afirmar ¡Que cada uno de esos cambios se ha realizado correctamente!

Tengo la idea de un concepto lógico, pero la conclusión inversa diría que ninguna función debe cambiar más de un objeto. Pero eso es imposible de implementar en todos los casos, en mi experiencia.

Tome el concepto lógico de una transacción bancaria: retirar un monto de una cuenta bancaria en la mayoría de los casos DEBE incluir agregar ese monto a otra cuenta. NUNCA quieres separar esas dos cosas, forman una unidad atómica. Es posible que desee realizar dos funciones (retirar/agregar dinero) y, por lo tanto, escribir dos pruebas unitarias diferentes, además. Pero esas dos acciones deben llevarse a cabo dentro de una transacción y también debe asegurarse de que la transacción funcione. En ese caso, simplemente no es suficiente para asegurarse de que los pasos individuales fueron exitosos. Tienes que verificar ambas cuentas bancarias, en tu prueba.

Puede haber ejemplos más complejos que no probaría en una prueba unitaria, en primer lugar, sino en una prueba de integración o aceptación. Pero esos límites son fluidos, en mi humilde opinión! No es tan fácil decidir, es una cuestión de circunstancias y quizás de preferencia personal. Retirar dinero de uno y agregarlo a otra cuenta sigue siendo una función muy simple y definitivamente un candidato para pruebas unitarias.

0
cslotty