2009-01-08 26 views
10

Actualmente estoy escribiendo algunas pruebas unitarias para una clase lógica de negocios que incluye rutinas de validación. Por ejemplo:Pruebas unitarias y lógica de validación

public User CreateUser(string username, string password, UserDetails details) 
{ 
    ValidateUserDetails(details); 
    ValidateUsername(username); 
    ValidatePassword(password); 

    // create and return user 
} 

Debe mi accesorio de prueba contener pruebas para cada posible error de validación que pueden ocurrir en los métodos Validar *, o es mejor dejar que por un conjunto separado de las pruebas? ¿O tal vez la lógica de validación debe ser refactorizada de alguna manera?

Mi razonamiento es que si decido probar todos los errores de validación que pueden ocurrir dentro de CreateUser, el dispositivo de prueba se volverá bastante hinchado. Y la mayoría de los métodos de validación se utilizan en más de un lugar ...

¿Algún buen patrón o sugerencia en este caso?

Respuesta

11

Todas las pruebas solo deben fallar por una razón y solo una prueba debe fallar por ese motivo.

Esto ayuda mucho con la escritura de un conjunto de pruebas de unidad que se puede mantener.

Escribiría un par de pruebas para ValidateUserDetails, ValidateUsername y ValidateUserPassword. Entonces solo necesita probar que CreateUser llama a esas funciones.


Lea su pregunta; Parece que entendí mal las cosas un poco.

Puede que le interese lo que J.P Boodhoo ha escrito sobre su estilo de diseño impulsado por el comportamiento. http://blog.developwithpassion.com/2008/12/22/how-im-currently-writing-my-bdd-style-tests-part-2/

BDD se está convirtiendo en un término muy sobrecargado, cada uno tiene una definición diferente y diferentes herramientas para hacerlo. Por lo que veo, lo que JP Boodhoo está haciendo es dividir los accesorios de prueba según la preocupación y no la clase.

Por ejemplo, podría crear accesorios separados para probar Validación de detalles de usuario, Validación de nombre de usuario, Validación de contraseña y creación de usuarios. La idea de BDD es que al nombrar las pruebas y pruebas de la manera correcta, puede crear algo que casi lea como documentación imprimiendo los nombres de las pruebas y los nombres de las pruebas. Otra ventaja de agrupar sus pruebas por preocupación y no por clase es que probablemente solo necesitará una configuración y rutina de desmontaje para cada accesorio.

Aunque no he tenido mucha experiencia con esto.

Si le interesa leer más, JP Boodhoo ha publicado mucho sobre esto en su blog (vea el enlace anterior) o también puede escuchar el episodio de dot net rocks con Scott Bellware donde habla de una manera similar de agrupar y nombrar pruebas http://www.dotnetrocks.com/default.aspx?showNum=406

Espero que esto sea más de lo que está buscando.

+0

He reformulado mi pregunta un poco - por favor lea de nuevo – JacobE

2
  • Let Unit Las pruebas (en plural) contra los métodos Validate confirman su correcto funcionamiento.
  • Let Unit Tests (en plural) contra el método CreateUser confirman su correcto funcionamiento.

Si CreateUser se requiere simplemente para llamar a los métodos de validación, pero no es necesario tomar decisiones de validación por sí mismo, entonces las pruebas contra CreateUser deben confirmar ese requisito.

2

Definitivamente debe probar los métodos de validación .

No es necesario probar otros métodos para todas las posibles combinaciones de argumentos solo para asegurarse de que se realiza la validación.

Parece que está mezclando Validación y diseño por contrato.

La validación se suele realizar para avisar al usuario que su entrada es incorrecta. Está muy relacionado con la lógica comercial (la contraseña no es lo suficientemente fuerte, el correo electrónico tiene un formato incorrecto, etc.).

El diseño por contrato asegura que su código se puede ejecutar sin tirar excepciones más adelante (incluso sin ellos podría obtener la excepción, pero mucho más tarde y probablemente más oscura).

En cuanto a la capa de aplicación que debe contener lógica de validación, probablemente la mejor es service layer (by Fowler) que define los límites de la aplicación y es un buen lugar para desinfectar la entrada de la aplicación. Y no debe haber ninguna lógica de validación dentro de estos límites, solo Diseñe por contrato para detectar errores antes.

Así que, finalmente, escriba pruebas de validación de la lógica cuando quiera notificar amigablemente al usuario que se ha equivocado. De lo contrario, use Design By Contract y siga arrojando excepciones.

0

Agregaría un montón de pruebas para cada método ValidateXXX. Luego, en CreateUser cree 3 casos de prueba para verificar qué ocurre cuando cada uno de ValidateUserDetails, ValidateUsername y ValidatePassword falla, pero el otro tiene éxito.

0

Estoy usando Lokad Shared Library para definir reglas de validación de negocios. He aquí cómo pruebo casos de esquina (muestra de la fuente abierta):

[Test] 
public void Test() 
{ 
    ShouldPass("[email protected]", "pwd", "http://ws.lokad.com/TimeSerieS2.asmx"); 
    ShouldPass("[email protected]", "pwd", "http://127.0.0.1/TimeSerieS2.asmx"); 
    ShouldPass("[email protected]", "pwd", "http://sandbox-ws.lokad.com/TimeSerieS2.asmx"); 

    ShouldFail("invalid", "pwd", "http://ws.lokad.com/TimeSerieS.asmx"); 
    ShouldFail("[email protected]", "pwd", "http://identity-theift.com/TimeSerieS2.asmx"); 
} 

static void ShouldFail(string username, string pwd, string url) 
{ 
    try 
    { 
    ShouldPass(username, pwd, url); 
    Assert.Fail("Expected {0}", typeof (RuleException).Name); 
    } 
    catch (RuleException) 
    { 
    } 
} 

static void ShouldPass(string username, string pwd, string url) 
{ 
    var connection = new ServiceConnection(username, pwd, new Uri(url)); 
    Enforce.That(connection, ApiRules.ValidConnection); 
} 

Dónde regla ValidConnection se define como:

public static void ValidConnection(ServiceConnection connection, IScope scope) 
{ 
    scope.Validate(connection.Username, "UserName", StringIs.Limited(6, 256), StringIs.ValidEmail); 
    scope.Validate(connection.Password, "Password", StringIs.Limited(1, 256)); 
    scope.Validate(connection.Endpoint, "Endpoint", Endpoint); 
} 

static void Endpoint(Uri obj, IScope scope) 
{ 
    var local = obj.LocalPath.ToLowerInvariant(); 
    if (local == "/timeseries.asmx") 
    { 
    scope.Error("Please, use TimeSeries2.asmx"); 
    } 
    else if (local != "/timeseries2.asmx") 
    { 
    scope.Error("Unsupported local address '{0}'", local); 
    } 

    if (!obj.IsLoopback) 
    { 
    var host = obj.Host.ToLowerInvariant(); 
    if ((host != "ws.lokad.com") && (host != "sandbox-ws.lokad.com")) 
     scope.Error("Unknown host '{0}'", host); 
    } 

Si se descubre algún caso en su defecto (es decir: la nueva URL de conexión válida es agregado), entonces la regla y la prueba se actualizan.

Más sobre este patrón se puede encontrar en this article. Todo es de código abierto, así que siéntase libre de reutilizar o hacer preguntas.

PS: en cuenta que reglas primitivas utilizados en esta regla compuesto de la muestra (es decir StringIs.ValidEmail o StringIs.Limited) se prueban a fondo en su propia y por lo tanto no necesita unidad excesivo pruebas.

2

¿Cuál es la responsabilidad de su clase de lógica de negocios y hace algo aparte de la validación? Creo que estaría tentado de mover las rutinas de validación a una clase propia (UserValidator) o múltiples clases (UserDetailsValidator + UserCredentialsValidator) dependiendo de su contexto y luego proporcionar simulaciones para las pruebas. Por lo que su clase ahora sería algo como:

public User CreateUser(string username, string password, UserDetails details) 
{ 
    if (Validator.isValid(details, username, password)) { 
     // what happens when not valid 
    } 

    // create and return user 
} 

A continuación, puede proporcionar pruebas unitarias separadas puramente para la validación y las pruebas para la clase de lógica de negocio puede centrarse en la hora de validación pasa y cuando falla la validación, así como todas tus otras pruebas.