2009-07-29 18 views
6

Estoy tratando de probar la unidad en un proyecto personal de PHP como un buen pequeño programador, y me gustaría hacerlo correctamente. Por lo que escuché, lo que se supone que debes probar es solo la interfaz pública de un método, pero me preguntaba si eso se aplicaría a continuación.¿Qué tan lejos debería llegar con las pruebas unitarias?

Tengo un método que genera un token de restablecimiento de contraseña en caso de que el usuario olvide su contraseña. El método devuelve una de dos cosas: nada (nulo) si todo funcionó bien, o un código de error que indica que el usuario con el nombre de usuario especificado no existe.

Si solo estoy probando la interfaz pública, ¿cómo puedo estar seguro de que el token de restablecimiento de contraseña va a la base de datos si el nombre de usuario es válido y NO va a la base de datos si el nombre de usuario NO es válido? ¿Debo realizar consultas en mis pruebas para validar esto? ¿O debería asumir que mi lógica es sólida?

Ahora, este método es muy simple y esto no es tan importante: el problema es que esta misma situación se aplica a muchos otros métodos. ¿Qué haces en las pruebas de unidad centrada en la base de datos?

Código, para referencia si es necesario:

public function generatePasswordReset($username) 
{ 
    $this->sql='SELECT id 
       FROM users 
       WHERE username = :username'; 

    $this->addParam(':username', $username); 
    $user=$this->query()->fetch(); 

    if (!$user) 
     return self::$E_USER_DOESNT_EXIST; 
    else 
    { 
     $code=md5(uniqid()); 
     $this->addParams(array(':uid'  => $user['id'], 
           ':code'  => $code, 
           ':duration' => 24 //in hours, how long reset is valid 
          )); 

     //generate new code, delete old one if present 
     $this->sql ='DELETE FROM password_resets WHERE user_id=:uid;'; 
     $this->sql.="INSERT INTO password_resets (user_id, code, expires) 
        VALUES  (:uid, :code, now() + interval ':duration hours')"; 

     $this->execute(); 
    } 
} 
+1

Lo mejor de las pruebas unitarias, al menos para mí, es que le muestra dónde necesita refactorizar. También ayuda a resaltar las dependencias. Sugeriría que su 'SELECT' y su' DELETE + INSERT' deberían refactorizarse en sus propios métodos, que la generación de contraseñas debería ser en su propia –

+0

@pcampbell - su comentario debería ser una respuesta –

Respuesta

6

Lo mejor de las pruebas unitarias, al menos para mí, es que le muestra dónde necesita refactorizar. Usando el código de ejemplo anterior, usted ha conseguido básicamente cuatro cosas que suceden en un método:

//1. get the user from the DB 
//2. in a big else, check if user is null 
//3. create a array containing the userID, a code, and expiry 
//4. delete any existing password resets 
//5. create a new password reset 

Prueba de la unidad también es muy bueno porque ayuda a resaltar las dependencias. Este método, como se muestra arriba, depende de un DB, en lugar de un objeto que implementa una interfaz. Este método interactúa con sistemas fuera de su alcance, y realmente solo podría probarse con una prueba de integración, en lugar de una prueba unitaria. Las pruebas unitarias son para garantizar el funcionamiento/la corrección de una unidad de trabajo.

Considere el Single Responsibility Principle: "Do one thing". Se aplica tanto a los métodos como a las clases.

me gustaría sugerir que su método generatePasswordReset debe ser readaptado a:

  • darse un pre-definido objeto de usuario/ID existente. Haga todos esos controles de cordura fuera de este método. Haz una cosa.
  • pon el código de restablecimiento de contraseña en su propio método. Esa sería una sola unidad de trabajo que podría probarse independientemente de SELECT, DELETE y INSERT.
  • Crea un nuevo método que podría llamarse OverwriteExistingPwdChangeRequests() que se ocuparía del BORRAR + INSERTAR.
+0

Entiendo lo que dices, pero aun así seguiré en el mismo barco. Si puedo probar todos los 2/3 métodos de forma individual, ¿cómo es eso diferente de probar un método que hace lo mismo? Esto se encuentra en mi clase de modelo de usuario. Suponiendo que coloque mis controles de cordura en el controlador, solo tendría que pasar mi prueba al lado del controlador. – ryeguy

0

En general se podría "simulacro" del objeto al que se acoge, verificando que recibe las solicitudes esperadas.

En este caso, no estoy seguro de lo útil que es, casi no termina escribiendo la misma lógica dos veces ... pensamos que enviamos "ELIMINAR de la contraseña" etc. ¡Oh, mira, lo hicimos!

Hmmm, lo que realmente comprobamos. Si la cuerda estuviera mal formada, ¡no lo sabríamos!

Puede estar en contra de la letra de la ley de pruebas de unidades, pero en cambio probaría estos efectos secundarios haciendo consultas separadas en la base de datos.

1

Puedes descomponerlo un poco más, esa función está haciendo mucho lo que hace que probarlo sea un poco complicado, no imposible pero complicado. Si, por otro lado, sacaste algunas funciones extra más pequeñas (getUserByUsername, deletePasswordByUserID, addPasswordByUserId, etc. Luego puedes probarlas con bastante facilidad una vez y saber que funcionan para que no tengas que volver a probarlas. De esta forma, prueba la más baja presiona las llamadas asegurándose de que estén en buen estado para que no tengas que preocuparte más por la cadena. Luego, para esta función, todo lo que tienes que hacer es lanzarlo a un usuario que no existe y asegurarte de que vuelva con un error USER_DOESNT_EXIST luego uno donde existe un usuario (aquí es donde prueba DB entra). Los trabajos internos ya han sido ejercidos en otro lugar (con suerte).

0

Probando la interfaz pública es necesario, pero no suficiente. Hay muchas filosofías sobre cuántas pruebas se requieren, y solo puedo dar mi opinión. Probar todo. Literalmente. Debería tener una prueba que verifique que cada línea de código ha sido ejercitado por el conjunto de pruebas. (Solo digo 'cada línea' porque estoy pensando en C y gcov, y gcov proporciona granularidad a nivel de línea. Si tienes una herramienta que tiene una resolución más fina, úsala). Si puedes agregar un trozo de código a tu base de código sin agregar una prueba, el conjunto de pruebas debe fallar.

3

La razón por la que esta función es más difícil de probar es porque la actualización de la base de datos es un efecto secundario de la función (es decir, no hay un retorno explícito para que la pruebe).

Una forma de lidiar con las actualizaciones de estado en objetos remotos como este es crear un objeto simulado que proporcione la misma interfaz que el DB (es decir, se ve idéntico desde la perspectiva de su código).Luego, en su prueba, puede verificar los cambios de estado dentro de este objeto simulado y confirmar que recibió lo que debería.

+0

¿Cuánto de la "base de datos "¿Necesita implementar para poder verificar esos cambios de estado? ¿Analizando SQL? ¿Manteniendo algunas tablas de imitación de datos? Mi opinión es que el uso de una base de datos real termina siendo menos trabajo. – djna

+0

Estoy de acuerdo en que hay dificultades con las pruebas unitarias en este caso particular. Está esencialmente probando meta-programación (creando SQL para manipular dinámicamente un modelo de estado complejo). Como han dicho otros, la función podría simplificarse. En este ejemplo particular, sin refactorización, probaría en dos etapas. 1-> Escribe una consulta SQL para hacer la actualización y verifica (a mano) que funcione en una base de datos real. 2-> Escriba la prueba unitaria para este código, codifique con fuerza la consulta SQL que probó en la parte 1, y verifique que el SQL que envía al DB coincida con la forma de su consulta probada. –

0

Las bases de datos son variables globales. Las variables globales son interfaces públicas por cada unidad que las usa. Por lo tanto, sus casos de prueba deben variar las entradas no solo en el parámetro de la función, sino también en las entradas de la base de datos.

0

Si las pruebas de su unidad tienen efectos secundarios (como cambiar una base de datos), entonces se han convertido en pruebas de integración. No hay nada malo en sí mismo con las pruebas de integración; cualquier prueba automatizada es buena para la calidad de su producto. Pero las pruebas de integración tienen un mayor costo de mantenimiento porque son más complejas y más fáciles de romper.

El truco está en minimizar el código que solo se puede probar con efectos secundarios. Aísle y oculte las consultas SQL en una clase separada MyDatabase que no contenga ninguna lógica comercial. Pase una instancia de este objeto a su código de lógica de negocios.

Luego, cuando pruebe su lógica comercial, puede sustituir el objeto MyDatabase con una instancia simulada que no esté conectada a una base de datos real y que pueda usarse para verificar que su código de lógica de negocios utilice la base de datos correctamente.

Consulte la documentación de SimpleTest (un marco de burla php) para un ejemplo.

1

Las pruebas unitarias sirven para verificar que una unidad funcione. Si le interesa saber si una unidad funciona o no, escriba una prueba. Es así de simple. Elegir escribir una prueba unitaria o no debe basarse en algún cuadro o regla general. Como profesional, es su responsabilidad entregar código de trabajo, y no puede saber si funciona o no, a menos que lo pruebe.

Ahora, eso no significa que escriba una prueba para cada línea de código. Tampoco significa necesariamente que escriba una prueba unitaria para cada función. La decisión de probar o no una determinada unidad de trabajo se reduce a riesgo. ¿Qué tan dispuesto está a arriesgar que se despliegue su pieza de código no probado?

Si se pregunta "¿cómo sé si funciona esta funcionalidad?", La respuesta es "no, hasta que tenga pruebas repetibles que demuestren que funciona".

Cuestiones relacionadas