c++ - ¿Cuál es la mejor manera de probar métodos privados con GoogleTest?



googletest c++ (1)

Hay al menos dos opciones más. Enumeraré algunas otras opciones que debería considerar al explicar una situación determinada.

Opción 4:

Considere refactorizar su código para que la parte que desea probar sea pública en otra clase. Normalmente, cuando estás tentado a probar el método privado de una clase, es un signo de mal diseño. Uno de los paternos (anti) más comunes que veo es lo que Michael Feathers llama una clase de "Iceberg". Las clases de "Iceberg" tienen un método público, y el resto son privadas (por lo que es tentador probar los métodos privados). Podría verse algo como esto:

Por ejemplo, es posible que desee probar GetNextToken() llamándolo en una cadena sucesivamente y viendo que devuelve el resultado esperado. Una función como esta justifica una prueba: ese comportamiento no es trivial, especialmente si las reglas de tokenización son complejas. Supongamos que no es tan complejo, y solo queremos atar en fichas delimitadas por el espacio. Así que escribes una prueba, tal vez se ve algo como esto:

TEST(RuleEvaluator, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar";
    RuleEvaluator re = RuleEvaluator(input_string);
    EXPECT_EQ(re.GetNextToken(), "1");
    EXPECT_EQ(re.GetNextToken(), "2");
    EXPECT_EQ(re.GetNextToken(), "test");
    EXPECT_EQ(re.GetNextToken(), "bar");
    EXPECT_EQ(re.HasMoreTokens(), false);
}

Bueno, en realidad se ve muy bien. Queremos asegurarnos de mantener este comportamiento mientras hacemos cambios. ¡Pero GetNextToken() es una función privada ! Así que no podemos probarlo así, porque ni siquiera se compila . Pero, ¿qué hay de cambiar la clase RuleEvaluator para seguir el Principio de Responsabilidad Única (Principio de Responsabilidad Única)? Por ejemplo, parece que tenemos un analizador, un tokenizador y un evaluador atascados en una clase. ¿No sería mejor simplemente separar esas responsabilidades? Además de eso, si creas una clase Tokenizer , entonces sus métodos públicos serían HasMoreTokens() y GetNextTokens() . La clase RuleEvaluator podría tener un objeto Tokenizer como miembro. Ahora, podemos mantener la misma prueba que la anterior, excepto que estamos probando la clase Tokenizer lugar de la clase RuleEvaluator .

Esto es lo que podría parecer en UML:

Tenga en cuenta que este nuevo diseño aumenta la modularidad, por lo que podría reutilizar estas clases en otras partes de su sistema (antes de que no pudiera, los métodos privados no son reutilizables por definición). Esta es la principal ventaja de descomponer el RuleEvaluator, junto con una mayor comprensión / localidad.

La prueba se vería extremadamente similar, excepto que en realidad se compilaría esta vez ya que el método GetNextToken() ahora es público en la clase Tokenizer :

TEST(Tokenizer, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar";
    Tokenizer tokenizer = Tokenizer(input_string);
    EXPECT_EQ(tokenizer.GetNextToken(), "1");
    EXPECT_EQ(tokenizer.GetNextToken(), "2");
    EXPECT_EQ(tokenizer.GetNextToken(), "test");
    EXPECT_EQ(tokenizer.GetNextToken(), "bar");
    EXPECT_EQ(tokenizer.HasMoreTokens(), false);
}

Opcion 5

Simplemente no pruebes las funciones privadas. A veces no vale la pena probarlos porque serán probados a través de la interfaz pública. Muchas veces lo que veo son pruebas que parecen muy similares, pero prueban dos funciones / métodos diferentes. Lo que termina sucediendo es que cuando los requisitos cambian (y siempre lo hacen), ahora tiene 2 pruebas rotas en lugar de 1. Y si realmente probó todos sus métodos privados, podría tener más de 10 pruebas rotas en lugar de 1. En resumen , probar funciones privadas (mediante FRIEND_TEST o hacerlas públicas) que de otro modo podrían probarse a través de una interfaz pública causaría la duplicación de pruebas . Realmente no quieres esto, porque nada duele más que tu conjunto de pruebas que te ralentiza. ¡Se supone que reduce el tiempo de desarrollo y los costos de mantenimiento! Si prueba métodos privados que de otro modo se prueban a través de una interfaz pública, el conjunto de pruebas puede hacer lo contrario e incrementar activamente los costos de mantenimiento e incrementar el tiempo de desarrollo. Cuando haces pública una función privada, o si usas algo como FRIEND_TEST , generalmente terminarás arrepintiéndote.

Considere la siguiente implementación posible de la clase Tokenizer :

Digamos que SplitUpByDelimiter() es responsable de devolver un std::vector<std::string> manera que cada elemento del vector sea un token. Además, digamos que GetNextToken() es simplemente un iterador de este vector. Así que tus pruebas podrían verse así:

TEST(Tokenizer, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar";
    Tokenizer tokenizer = Tokenizer(input_string);
    EXPECT_EQ(tokenizer.GetNextToken(), "1");
    EXPECT_EQ(tokenizer.GetNextToken(), "2");
    EXPECT_EQ(tokenizer.GetNextToken(), "test");
    EXPECT_EQ(tokenizer.GetNextToken(), "bar");
    EXPECT_EQ(tokenizer.HasMoreTokens(), false);
}

// Pretend we have some class for a FRIEND_TEST
TEST_F(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar";
    Tokenizer tokenizer = Tokenizer(input_string);
    std::vector<std::string> result = tokenizer.SplitUpByDelimiter(" ");
    EXPECT_EQ(result.size(), 4);
    EXPECT_EQ(result[0], "1");
    EXPECT_EQ(result[1], "2");
    EXPECT_EQ(result[2], "test");
    EXPECT_EQ(result[3], "bar");
}

Bueno, ahora digamos que los requisitos cambian, y ahora se espera que se analicen con un "," en lugar de un espacio. Naturalmente, usted esperará que se rompa una prueba, pero el dolor aumenta cuando prueba las funciones privadas. OMI, Google Test no debe permitir FRIEND_TEST. Casi nunca es lo que quieres hacer. Michael Feathers se refiere a cosas como FRIEND_TEST como una "herramienta de FRIEND_TEST ", ya que trata de tocar las partes privadas de otra persona.

Recomiendo evitar las opciones 1 y 2 cuando pueda, ya que generalmente causa "duplicación de prueba" y, como consecuencia, muchas más pruebas de las necesarias se interrumpirán cuando cambien los requisitos. Úsalos como último recurso. Las opciones 1 y 2 son las formas más rápidas de "probar métodos privados" aquí y ahora (como en la implementación más rápida), pero realmente dañarán la productividad a largo plazo.

PIMPL también puede tener sentido, pero todavía permite un diseño bastante malo. Ten cuidado con ello

Recomiendo la Opción 4 (refactorización en componentes verificables más pequeños) como el lugar correcto para comenzar, pero a veces lo que realmente desea es la Opción 5 (probar las funciones privadas a través de la interfaz pública).

PS Aquí está la conferencia relevante sobre las clases de iceberg: https://www.youtube.com/watch?v=4cVZvoFGJTU

PSS En cuanto a todo en software, la respuesta es que depende . No hay una talla para todos. La opción que resuelva su problema dependerá de sus circunstancias específicas .

Me gustaría probar algunos métodos privados utilizando GoogleTest.

class Foo
{
private:
    int bar(...)
}

GoogleTest permite un par de maneras de hacer esto.

OPCIÓN 1

Con FRIEND_TEST :

class Foo
{
private:
    FRIEND_TEST(Foo, barReturnsZero);
    int bar(...);
}

TEST(Foo, barReturnsZero)
{
    Foo foo;
    EXPECT_EQ(foo.bar(...), 0);
}

Esto implica incluir "gtest / gtest.h" en el archivo fuente de producción.

OPCION 2

Declare un dispositivo de prueba como amigo de la clase y defina los accesores en el dispositivo:

class Foo
{
    friend class FooTest;
private:
    int bar(...);
}

class FooTest : public ::testing::Test
{
protected:
    int bar(...) { foo.bar(...); }
private:
    Foo foo;
}

TEST_F(FooTest, barReturnsZero)
{
    EXPECT_EQ(bar(...), 0);
}

OPCION 3

El lenguaje Pimpl .

Para más detalles: Google Test: Guía avanzada .

¿Existen otras formas de probar métodos privados? ¿Cuáles son algunas ventajas y desventajas de cada opción?





googletest