No me gusta probar la funcionalidad privada por un par de razones. Son los siguientes (estos son los puntos principales para las personas TLDR):
- Por lo general, cuando estás tentado a probar el método privado de una clase, es un olor a diseño.
- Puede probarlos a través de la interfaz pública (que es cómo desea probarlos, porque así es como los llamará / usará el cliente). Puede obtener una falsa sensación de seguridad al ver la luz verde en todas las pruebas de aprobación de sus métodos privados. Es mucho mejor / más seguro probar casos extremos en sus funciones privadas a través de su interfaz pública.
- Corre el riesgo de una duplicación severa de la prueba (pruebas que se ven / se sienten muy similares) al probar métodos privados. Esto tiene consecuencias importantes cuando cambian los requisitos, ya que se romperán muchas más pruebas de las necesarias. También puede ponerlo en una posición en la que es difícil refactorizar debido a su conjunto de pruebas ... lo cual es la ironía final, ¡porque el conjunto de pruebas está ahí para ayudarlo a rediseñar y refactorizar de manera segura!
Explicaré cada uno de estos con un ejemplo concreto. Resulta que 2) y 3) están algo intrincadamente conectados, por lo que su ejemplo es similar, aunque considero que son razones separadas por las que no debe probar métodos privados.
Hay momentos en los que es apropiado probar métodos privados, solo es importante tener en cuenta los inconvenientes mencionados anteriormente. Voy a repasarlo con más detalle más adelante.
También repaso por qué TDD no es una excusa válida para probar métodos privados al final.
Refactorizando su salida de un mal diseño
Uno de los patrones (anti) más comunes que veo es lo que Michael Feathers llama una clase "Iceberg" (si no sabe quién es Michael Feathers, vaya a comprar / lea su libro "Trabajando eficazmente con el código heredado". Él es una persona que vale la pena conocer si es un ingeniero / desarrollador de software profesional). Hay otros patrones (anti) que hacen que este problema surja, pero este es, con mucho, el más común con el que me he encontrado. Las clases "Iceberg" tienen un método público, y el resto son privadas (por eso es tentador probar los métodos privados). Se llama clase "Iceberg" porque generalmente hay un método público solitario que aparece, pero el resto de la funcionalidad está oculta bajo el agua en forma de métodos privados.
Por ejemplo, es posible que desee probar GetNextToken()
llamándolo sucesivamente a una cadena y observando que devuelve el resultado esperado. Una función como esta garantiza una prueba: ese comportamiento no es trivial, especialmente si sus reglas de tokenización son complejas. Supongamos que no es tan complejo, y solo queremos enredar en tokens delimitados por el espacio. Entonces escribes una prueba, tal vez se ve más o menos así (algún código psuedo agnóstico de lenguaje, espero que la idea sea clara)
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
re = RuleEvaluator(input_string);
ASSERT re.GetNextToken() IS "1";
ASSERT re.GetNextToken() IS "2";
ASSERT re.GetNextToken() IS "test";
ASSERT re.GetNextToken() IS "bar";
ASSERT re.HasMoreTokens() IS FALSE;
}
Bueno, eso en realidad se ve muy bien. Queremos asegurarnos de mantener este comportamiento a medida que hacemos cambios. ¡Pero GetNextToken()
es una función privada ! Por lo tanto, no podemos probarlo de esta manera, ya que ni siquiera se compilará (suponiendo que estemos usando algún lenguaje que realmente aplique lo público / privado, a diferencia de algunos lenguajes de script como Python). Pero, ¿qué hay de cambiar la RuleEvaluator
clase para seguir el Principio de responsabilidad única (Principio de responsabilidad única)? Por ejemplo, parece que tenemos un analizador, un tokenizador y un evaluador agrupados en una clase. ¿No sería mejor separar esas responsabilidades? Además de eso, si crea una Tokenizer
clase, entonces sus métodos públicos serían HasMoreTokens()
yGetNextTokens()
. losRuleEvaluator
clase podría tener unTokenizer
objeto como miembro. Ahora, podemos mantener la misma prueba anterior, excepto que estamos probando la Tokenizer
clase en lugar de la RuleEvaluator
clase.
Así es como se vería 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 romper 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 GetNextToken()
método ahora es público en la Tokenizer
clase:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS FALSE;
}
Probar componentes privados a través de una interfaz pública y evitar la duplicación de pruebas
Incluso si no cree que puede dividir su problema en menos componentes modulares (que puede hacer el 95% del tiempo si solo intenta hacerlo), simplemente puede probar las funciones privadas a través de una interfaz pública. Muchas veces no vale la pena probar a los miembros privados porque serán probados a través de la interfaz pública. Muchas veces lo que veo son pruebas que se ven muy , 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. o hacer ellos públicos o usando la reflexión) que de otra manera podrían ser probados a través de una interfaz pública pueden causar duplicación de prueba En resumen , probar funciones privadas (medianteFRIEND_TEST
. Realmente no quieres esto, porque nada duele más que tu conjunto de pruebas que te frena. ¡Se supone que disminuye 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 y aumentar activamente los costos de mantenimiento y el tiempo de desarrollo. Cuando haces pública una función privada, o si usas algo comoFRIEND_TEST
y / o reflexión, generalmente terminará arrepintiéndose a la larga.
Considere la siguiente implementación posible de la Tokenizer
clase:
Digamos que SplitUpByDelimiter()
es responsable de devolver una matriz de manera que cada elemento de la matriz sea un token. Además, digamos que GetNextToken()
es simplemente un iterador sobre este vector. Entonces su prueba pública podría verse así:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
ASSERT tokenizer.GetNextToken() IS "1";
ASSERT tokenizer.GetNextToken() IS "2";
ASSERT tokenizer.GetNextToken() IS "test";
ASSERT tokenizer.GetNextToken() IS "bar";
ASSERT tokenizer.HasMoreTokens() IS false;
}
Supongamos que tenemos lo que Michael Feather llama una herramienta a tientas . Esta es una herramienta que te permite tocar las partes privadas de otras personas. Un ejemplo es FRIEND_TEST
de googletest, o reflexión si el idioma lo admite.
TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
input_string = "1 2 test bar"
tokenizer = Tokenizer(input_string);
result_array = tokenizer.SplitUpByDelimiter(" ");
ASSERT result.size() IS 4;
ASSERT result[0] IS "1";
ASSERT result[1] IS "2";
ASSERT result[2] IS "test";
ASSERT result[3] IS "bar";
}
Bueno, ahora digamos que los requisitos cambian, y la tokenización se vuelve mucho más compleja. Decide que un delimitador de cadena simple no será suficiente y necesita una Delimiter
clase para manejar el trabajo. Naturalmente, esperará que se rompa una prueba, pero ese dolor aumenta cuando prueba funciones privadas.
¿Cuándo pueden ser apropiados los métodos privados de prueba?
No hay una "talla única para todos" en el software. A veces está bien (y en realidad es ideal) "romper las reglas". Recomiendo no probar la funcionalidad privada cuando pueda. Hay dos situaciones principales cuando creo que está bien:
He trabajado mucho con sistemas heredados (por eso soy tan fanático de Michael Feathers), y puedo decir con seguridad que a veces es más seguro simplemente probar la funcionalidad privada. Puede ser especialmente útil para obtener "pruebas de caracterización" en la línea de base.
Tienes prisa y tienes que hacer lo más rápido posible aquí y ahora. A la larga, no desea probar métodos privados. Pero diré que generalmente toma algún tiempo refactorizar para abordar los problemas de diseño. Y a veces tienes que enviar en una semana. Eso está bien: haz lo rápido y sucio y prueba los métodos privados usando una herramienta de búsqueda a tientas si eso es lo que crees que es la forma más rápida y confiable de hacer el trabajo. Pero comprenda que lo que hizo fue subóptimo a largo plazo, y considere volver a hacerlo (o, si se olvidó pero lo ve más tarde, corríjalo).
Probablemente hay otras situaciones en las que está bien. Si crees que está bien y tienes una buena justificación, entonces hazlo. Nadie te está deteniendo. Solo tenga en cuenta los costos potenciales.
La excusa TDD
Por otro lado, realmente no me gusta la gente que usa TDD como excusa para probar métodos privados. Practico TDD, y no creo que TDD te obligue a hacerlo. Puede escribir su prueba (para su interfaz pública) primero, y luego escribir código para satisfacer esa interfaz. A veces escribo una prueba para una interfaz pública, y la satisfaré escribiendo uno o dos métodos privados más pequeños (pero no pruebo los métodos privados directamente, pero sé que funcionan o mi prueba pública no funcionará) ) Si necesito probar casos extremos de ese método privado, escribiré un montón de pruebas que los afectarán a través de mi interfaz pública.Si no puede descubrir cómo llegar a los casos límite, esta es una buena señal de que necesita refactorizar en componentes pequeños, cada uno con sus propios métodos públicos. Es una señal de que sus funciones privadas están haciendo demasiado, y fuera del alcance de la clase .
Además, a veces encuentro que escribo una prueba que es demasiado grande para masticar en este momento, por lo que pienso "eh volveré a esa prueba más tarde cuando tenga más API para trabajar" (I Lo comentaré y lo mantendré en el fondo de mi mente). Aquí es donde muchos desarrolladores que he conocido comenzarán a escribir pruebas para su funcionalidad privada, utilizando TDD como chivo expiatorio. Dicen "oh, bueno, necesito otra prueba, pero para escribir esa prueba, necesitaré estos métodos privados. Por lo tanto, como no puedo escribir ningún código de producción sin escribir una prueba, necesito escribir una prueba por un método privado ". Pero lo que realmente necesitan hacer es refactorizar en componentes más pequeños y reutilizables en lugar de agregar / probar un montón de métodos privados a su clase actual.
Nota:
Respondí una pregunta similar acerca de probar métodos privados usando GoogleTest hace un tiempo. Principalmente modifiqué esa respuesta para que sea más independiente del lenguaje aquí.
PD: Aquí está la conferencia relevante sobre las clases de iceberg y las herramientas a tientas de Michael Feathers: https://www.youtube.com/watch?v=4cVZvoFGJTU