2010-01-24 21 views
7

Mi compañía está en una patada de Prueba de unidad, y estoy teniendo problemas con la refactorización del código de Service Layer. Aquí está un ejemplo de un código que escribí:Servicio de refactorización Clases de capa

public class InvoiceCalculator:IInvoiceCalculator 
{ 
    public CalculateInvoice(Invoice invoice) 
    { 
     foreach (InvoiceLine il in invoice.Lines) 
     { 
      UpdateLine(il); 
     } 
     //do a ton of other stuff here 
    } 

    private UpdateLine(InvoiceLine line) 
    { 
     line.Amount = line.Qty * line.Rate; 
     //do a bunch of other stuff, including calls to other private methods 
    } 
} 

En este caso simplificado (que se reduce de una clase 1000 línea que tiene 1 método público y ~ 30 privadas), mi jefe dice que debería ser capaz de probar mi CalculateInvoice y UpdateLine por separado (UpdateLine realmente llama a otros 3 métodos privados y también realiza llamadas a la base de datos). ¿Pero cómo haría esto? Su refactorización sugerido parecía un poco complicado para mí:

//Tiny part of original code 
public class InvoiceCalculator:IInvoiceCalculator 
{ 
    public ILineUpdater _lineUpdater; 

    public InvoiceCalculator (ILineUpdater lineUpdater) 
    { 
     _lineUpdater = lineUpdater; 
    } 

    public CalculateInvoice(Invoice invoice) 
    { 
     foreach (InvoiceLine il in invoice.Lines) 
     { 
      _lineUpdater.UpdateLine(il); 
     } 
     //do a ton of other stuff here 
    } 
} 

public class LineUpdater:ILineUpdater 
{ 
    public UpdateLine(InvoiceLine line) 
    { 
     line.Amount = line.Qty * line.Rate; 
     //do a bunch of other stuff 
    } 
} 

puedo ver cómo la dependencia se ha roto, y puedo probar ambas piezas, pero esto también crearía 20-30 clases extra de mi clase original. Solo calculamos las facturas en un solo lugar, por lo que estas piezas no serían realmente reutilizables. ¿Es esta la manera correcta de hacer este cambio, o sugerirías que haga algo diferente?

¡Gracias!

Jess

Respuesta

1

OMI todo esto depende de la forma 'significativa' ese método UpdateLine() es en realidad. Si solo se trata de un detalle de implementación (por ejemplo, podría incluirse fácilmente dentro del método CalculateInvoice() y lo único que dañaría es la legibilidad), entonces probablemente no es necesario que la unidad lo pruebe por separado de la clase maestra.

Por otro lado, si el método UpdateLine() tiene algún valor para la lógica comercial, si puede imaginar una situación en la que necesitaría cambiar este método independientemente del resto de la clase (y por lo tanto, probarlo por separado), entonces deberías continuar con la refactorización a una clase LineUpdater separada.

Probablemente no termines con 20-30 clases de esta manera, porque la mayoría de esos métodos privados son realmente solo detalles de implementación y no merecen probarse por separado.

0

yeah, nice one. No estoy seguro de si el objeto InvoiceLine también tiene alguna lógica incluida; de lo contrario, probablemente también necesite una IInvoiceLine. A veces tengo las mismas preguntas. Por un lado, quiere hacer las cosas bien y probar su código en una sola unidad, pero cuando se trata de invocar bases de datos y de escribir archivos, se requiere mucho trabajo adicional para configurar la primera prueba con todos los testobjects que intervienen cuando se trata de la creación de archivos y bases de datos. suceder, interfaces, afirma y también desea probar que la capa de datos no contiene ningún error. Entonces una prueba que es más 'proceso' luego 'unidad' es a menudo más fácil de construir. Si tiene un proyecto que se cambiará mucho (en el futuro) y muchas dependencias de este código (tal vez otros programas lean el archivo o los datos de la base de datos) puede ser bueno tener una prueba de unidad sólida para todas las partes de su código, y el tiempo de inversión vale la pena. Pero si el proyecto es, como mi último cliente me dice 'hagámoslo en vivo y quizás modifiquemos un poco el próximo año y el próximo año habrá algo nuevo', entonces no sería tan difícil para mí mismo obtener todo Pruebas unitarias en funcionamiento.

Michel

1

Bueno, su jefe va de manera más correcta en términos de pruebas unitarias:
Ahora puede probar CalculateInvoice() sin probar la función UpdateLine(). Puede pasar el objeto falso en lugar del objeto LineUpdater real y probar solo CalculateInvoice(), no un montón de código.
¿Es correcto? Depende. Tu jefe quiere hacer pruebas unitarias reales. Y las pruebas en su primer ejemplo no serían pruebas unitarias, serían pruebas de integración.

¿Cuáles son las ventajas de las pruebas unitarias antes de las pruebas de integración?
1) Las pruebas unitarias le permiten probar solo un método o propiedad, sin que se vean afectados por otros métodos/base de datos, etc.
2) Segunda ventaja: las pruebas unitarias se ejecutan más rápido (por ejemplo, dijo que UpdateLine usa la base de datos) porque no prueban todos los métodos anidados. Los métodos anidados pueden ser llamadas a la base de datos, de modo que si tienes miles de pruebas, tus pruebas pueden ejecutarse lentamente (varios minutos).
3) Tercera ventaja: si sus métodos hacen llamadas a la base de datos, a veces necesita configurar la base de datos (llenarla con los datos necesarios para la prueba) y puede no ser fácil - tal vez tendrá que escribir un par de páginas de código solo para preparar la base de datos para una prueba. Con las pruebas unitarias, se separan las llamadas a la base de datos de los métodos que se están probando (utilizando objetos simulados).

¡Pero! No estoy diciendo que la unidad pruebe un mejor. Ellos son simplemente diferentes. Como dije, las pruebas unitarias le permiten probar una unidad de forma aislada y rápida. Las pruebas de integración son más fáciles y le permiten probar los resultados de un trabajo conjunto de diferentes métodos y capas. Honestamente, prefiero las pruebas de integración :)

Además, tengo un par de sugerencias para usted:
1) No creo que tener un campo de cantidad es una buena idea. Parece que el campo Importe es adicional porque su valor se puede calcular en base a otros 2 campos públicos. Si desea hacerlo de todos modos, lo haría como una propiedad de solo lectura que devuelve Qty * Rate.
2) Por lo general, tener una clase que consta de 1000 filas puede significar que está mal diseñado y debe refactorizarse.

Ahora, espero que entiendas mejor la situación y puedas decidir. Además, si comprende la situación, puede hablar con su jefe y decidir juntos.

0

El ejemplo de su jefe me parece razonable.

Algunas de las consideraciones clave que trato de tener en cuenta en el diseño para cualquier escenario son:

  1. Responsabilidad Individual Principio

    Una clase sólo debe cambiar por una razón.

  2. ¿Cada nueva clase justificar su existencia

    tienen clases sido creado sólo para el bien de ella, o ellos encapsular una porción significativa de la lógica?

  3. ¿Puedes probar cada código de forma aislada?

En su escenario, simplemente mirando los nombres, parecería que estuviera vagando lejos de Responsabilidad Individual - Usted tiene una IInvoiceCalculator, sin embargo, esta clase es entonces también responsable de la actualización InvoiceLines. No solo ha hecho que sea muy difícil probar el comportamiento de actualización, sino que ahora necesita cambiar su clase InvoiceCalculator cuando las reglas de cálculo de negocio cambian y cuando cambian las reglas sobre la actualización.

Luego está la pregunta sobre la lógica de actualización: ¿justifica la lógica una clase separada? Eso realmente depende y es difícil de decir sin ver el código, pero ciertamente el hecho de que su jefe quiera que la lógica bajo prueba sugiera que es más que una simple llamada de línea a una capa de datos.

Usted dice que esta refactorización crea un gran número de clases adicionales, (estoy tomando eso en todas las entidades comerciales, ya que solo veo un par de clases nuevas y sus interfaces en su ejemplo) pero tiene para considerar lo que obtienes de esto. Parece que obtiene la capacidad de prueba completa de su código, la capacidad de introducir nuevos cálculos y la nueva lógica de actualización de forma aislada, y una encapsulación más clara de lo que son elementos separados de la lógica empresarial.

Los ganancias arriba son, por supuesto, sujeta a un análisis de rentabilidad, pero desde su abucheos está pidiendo para ellos, parece que él es feliz que van a pagar, contra el trabajo adicional para implementar el código de esta manera .

El tercer punto final, sobre las pruebas de manera aislada es también un beneficio clave de la manera que su jefe ha diseñado este - cuanto más cerca sus métodos públicos son para el código que hace que el trabajo podía comprender, más fácil es para inyectar talones o se burla de las partes de su sistema que no están bajo prueba. Por ejemplo, si está probando un método de actualización que desactive la capa de datos, no desea probar la capa de datos, por lo que normalmente se inyectaría una simulación. Si primero necesita pasar esa capa de datos burlada a través de toda la lógica de la calculadora, la configuración de la prueba será mucho más complicada ya que la simulación ahora necesita cumplir muchos otros requisitos potenciales, no relacionados con la prueba real en cuestión.

Si bien este enfoque es un trabajo extra inicialmente, diría que la mayoría del trabajo es el momento de pensar en el diseño, después de eso, y después de ponerse al día con el estilo de código basado en inyección, el tiempo de implementación sin procesar del software que está estructurado de esa manera es realmente comparable.

5

Este es un ejemplo de Feature Envy:

line.Amount = line.Qty * line.Rate; 

Debe probablemente se vería más como:

var amount = line.CalculateAmount(); 

No hay nada malo con un montón de clases, no se trata de volver a la facilidad de uso tanto como se trata de adaptabilidad. Cuando tiene muchas clases de responsabilidad únicas, es más fácil ver el comportamiento de su sistema y cambiarlo cuando cambian sus requisitos. Las grandes clases tienen responsabilidades entrelazadas que hacen que sea muy difícil cambiar.

+0

Todos nuestros objetos de entidades son básicamente DTO, por lo que toda la lógica de negocios está en clases de servicio. Pero hay mucha otra lógica en la calculadora de la línea de factura, simplemente le mostré esa pieza. –

0

El enfoque de sus hospitales es un gran ejemplo de inyección de dependencia y cómo hacerlo le permite usar un simulacro ILineUpdater para realizar sus pruebas de manera eficiente.

Cuestiones relacionadas