2012-03-11 17 views
8

Comenzaré diciendo que soy bastante nuevo en pruebas unitarias y me gustaría comenzar a usar un enfoque TDD, pero por el momento estoy escribiendo pruebas unitarias para algunas clases existentes para verificar su funcionalidad en todos los casos.¿Cómo se prueba que se llamó un método dentro de una clase bajo prueba?

He podido probar la mayoría de mi código usando NUnit y Rhino se burla sin muchos problemas. Sin embargo, me he estado preguntando acerca de las funciones de pruebas unitarias que terminan llamando a muchos otros métodos dentro de la misma clase. No puedo hacer algo como

classUnderTest.AssertWasCalled(cut => cut.SomeMethod(someArgs)) 

porque la clase bajo prueba no es falsa. Además, si un método que estoy probando llama a otros métodos en la clase bajo prueba que a su vez también llaman a métodos en la misma clase, voy a necesitar falsificar una tonelada de valores solo para probar el método de "nivel superior". Dado que también estoy probando de forma unitaria todos estos "submétodos", debería poder asumir que "SomeMethod" funciona como se espera si pasa la prueba de la unidad y no necesita preocuparse por los detalles de esos métodos de nivel inferior.

Aquí es un código de ejemplo que he estado trabajando con para ayudar a ilustrar mi punto (He escrito una clase para manejar la importación/exportación de archivos Excel utilizando NPOI):

public DataSet ExportExcelDocToDataSet(bool headerRowProvided) 
    { 
     DataSet ds = new DataSet(); 

     for (int i = 0; i < currentWorkbook.NumberOfSheets; i++) 
     {    
      ISheet tmpSheet = currentWorkbook.GetSheetAt(i); 

      if (tmpSheet.PhysicalNumberOfRows == 0) { continue; } 
      DataTable dt = GetDataTableFromExcelSheet(headerRowProvided, ds, tmpSheet); 

      if (dt.Rows.Count > 0) 
      { 
       AddNonEmptyTableToDataSet(ds, dt); 
      } 
     } 

     return ds; 
    } 

    public DataTable GetDataTableFromExcelSheet(bool headerRowProvided, DataSet ds, ISheet tmpSheet) 
    { 
     DataTable dt = new DataTable(); 
     for (int sheetRowIndex = 0; sheetRowIndex <= tmpSheet.LastRowNum; sheetRowIndex++) 
     { 
      DataRow dataRow = GetDataRowFromExcelRow(dt, tmpSheet, headerRowProvided, sheetRowIndex); 
      if (dataRow != null && dataRow.ItemArray.Count<object>(obj => obj != DBNull.Value) > 0) 
      { 
       dt.Rows.Add(dataRow); 
      } 
     } 

     return dt; 
    } 

... 

Se puede ver que ExportExcelDocToDataSet (mi método de "alto nivel" en este caso) llama GetDataTableFromExcelSheet que exige GetDataRowFromExcelRow, que llama a un par de otros métodos que se definen dentro de esta misma clase.

Entonces, ¿cuál es la estrategia recomendada para refaccionar este código para que sea más comprobable por unidad sin tener que insertar valores llamados por submétodos? ¿Hay alguna manera de falsificar llamadas a métodos dentro de la clase bajo prueba?

Gracias de antemano por cualquier ayuda o consejo!

Respuesta

6

Modificar el asunto under test (SUT). Si algo es difícil de probar por unidad, entonces el diseño puede ser incómodo.

Llamadas a métodos falsos dentro de la clase bajo prueba conduce a pruebas sobre especificadas. El resultado son pruebas muy frágiles: tan pronto como modifique o reforme la clase, es muy probable que también necesite modificar las pruebas unitarias. Esto conlleva costos de mantenimiento demasiado elevados para las pruebas unitarias.

Para evitar pruebas excesivas, concéntrese en los métodos públicos. Si este método llama a otros métodos dentro de la clase, no pruebe estas llamadas. Por otro lado: se deben probar las llamadas a métodos en otros dependend on component (DOCs).

Si te apegas a eso y tienes la sensación de que te pierdes algo importante en tus pruebas, entonces puede ser una señal para una clase o un método que está haciendo demasiado. En caso de una clase: busque violaciones del Single Responsibility Principle (SRP). Extraiga las clases y pruébelas por separado. En caso de un método: divida el método en varios métodos públicos y pruebe cada uno de ellos por separado. Si esto todavía es demasiado incómodo, definitivamente tienes una clase que viola el SRP.

En su caso específico se puede hacer lo siguiente: Extraer los métodos y ExportExcelDocToDataSetGetDataTableFromExcelSheet en dos clases diferentes (tal vez llamarlos ExcelToDataSetExporter y ExcelSheetToDataTableExporter). La clase original que contenía ambos métodos debe hacer referencia a ambas clases y llamar a esos métodos, que extrajiste previamente. Ahora puedes probar las tres clases de forma aislada. Aplique el Extract Class refactoring (book) para lograr la modificación de su clase original.

También tenga en cuenta que las pruebas de actualización siempre son un poco difíciles de escribir y mantener. La razón es que los SUT, que se escriben sin pruebas unitarias, tienden a tener un diseño incómodo y, por lo tanto, son más difíciles de probar. Esto significa que los problemas con las pruebas unitarias deben resolverse modificando los SUT y no pueden resolverse aumentando las pruebas unitarias.

+0

Pensé en extraer los métodos que mencionaste en clases separadas para poder probarlos en una unidad usando la extensión AssertWasCalled, pero no estaba seguro de si esta era una gran idea porque podía ver eso dando lugar a un montón de clases muy superficiales con uno o dos métodos en ellos como máximo. Definitivamente revisaré el libro que recomendó y también más sobre el SRP. Lo mejor de todo esto es que soy la única persona en el proyecto y todo el código ha sido escrito por mí, por lo que puedo hacer lo que sea necesario para que funcione. –

+1

@Gage Trader: no mida una clase en términos de números de métodos. Por ejemplo, el patrón de comando expone solo un método y todavía es una técnica utilizada ampliamente. Antes de aplicar pruebas unitarias y buenos principios de diseño OO, no me sentía cómodo con la mayor cantidad de clases. Me tomó un tiempo darme cuenta de que un mayor número de clases no significa más trabajo. Sí, es cierto que necesita más tiempo para el diseño y las pruebas, pero la mayor calidad y el menor número de errores lo compensa con creces. –

1

supongo que está probando el método público GetDataTableFromExcelSheet por separado, por lo que para las pruebas de ExportExcelDocToDataSet que no es necesario para verificar el comportamiento de GetDataTableFromExcelSheet (más allá del hecho de que ExportExcelDocToDataSet funciona como se espera).

Una estrategia común es probar solo los métodos públicos, ya que cualquier método privado que soporte sus métodos públicos se prueba de forma predeterminada si los métodos públicos se comportan como se espera.

Tomando esto en cuenta, puede probar solo los comportamientos de una clase, en lugar de enfocarse en métodos como la unidad. Eso ayuda a evitar que sus pruebas se vuelvan frágiles, donde el cambio de las partes internas de la clase tiene una tendencia a romper algunas de sus pruebas.

Por supuesto que desea que todo el código esté bien probado, pero un enfoque demasiado apretado en los métodos puede conducir a la fragilidad; el comportamiento de clase de prueba (¿hace lo que debería en el nivel más alto) también prueba los niveles más bajos.

Si quiere falsificar métodos de una prueba, podría refactorizar su código para tomar una interfaz para el método que quiere simular. Vea el command pattern.

En este caso, aunque el cambio obvio sería para el ExportExcelDocToDataSet tomar un libro de trabajo como argumento. En las pruebas, puede enviar un libro de trabajo falso. Ver inversion of control.

+0

Tengo un constructor que toma un objeto IWorkbook para burlarse del objeto del libro (no mostré ningún constructor en mi ejemplo), por lo que esa parte se soluciona. GetDataTableFromExcelSheet sería privado si no fuera por mí el querer probarlo. Veo lo que está diciendo acerca de probar solo los métodos públicos: he estado abriendo muchos de mis métodos previamente privados para las pruebas debido a la complejidad de algunos de los métodos en los niveles superiores. Me gusta mucho su consejo porque estaba pasando mucho tiempo escribiendo pruebas para cada método que tenía, que probablemente no sea la forma correcta de hacerlo. –

2

En realidad no importa qué método probado llama bajo el capó - esto es detalles de implementación y las pruebas de su unidad no deben ser mucho conscientes de eso. Por lo general (bueno, la mayoría de las veces con pruebas unitarias) desea probar unidad individual y centrarse en eso.

Puede escribir pruebas separadas y aisladas para cada método público en su clase o refactorizar parte de la funcionalidad de su clase probada en el exterior. Ambos enfoques se enfocan en lo mismo: tener pruebas aisladas para cada unidad .

Ahora, para darle algunos consejos:

  • ¿cuál es el nombre de su clase probado? ¿Basándose en los métodos que expone, algo en la línea de ExcelExporterAndToDataSetConverter ... o ExcelManager? Parece que esta clase podría estar haciendo too many things at once; esto pide un poco de refactorización. La exportación de datos a DataSet se puede separar fácilmente de la conversión de datos de Excel a DataSets/DataRows.
  • ¿Qué ocurre cuando cambia el método GetDataTableFromExcelSheet? ¿Se mueve a otra clase o se reemplaza por un código de terceros? ¿Debería romper tus pruebas de exportación? No debería ser: esta es una de las razones por las que sus pruebas de exportación no deberían verificar si se llamó o no.

Sugiero pasar a los métodos de conversión DataSet/DataRow para separar la clase: facilitará la escritura de las pruebas unitarias y sus pruebas de exportación no serán tan frágiles.

+0

Buena conjetura sobre el nombre de la clase: Se llama "ExcelHelper" y la idea es que sea una clase singular llamada por el resto de mi aplicación para tratar cualquier cosa relacionada con Excel. Probablemente debería crear al menos una clase ExcelImporter y ExcelExporter, y después de leer estos comentarios, tengo algunas ideas para dividir aún más el código. Siempre he pensado en refactorizar en términos de dividir el código en métodos más pequeños y privados en la misma clase para manejar el trabajo en lugar de escribir más clases con propósitos específicos. –

+1

@GageTrader: * helpers *, * managers * y todas las clases con nombres vagos generalmente indican una violación de SRP. 'ExcelImporter' comunica su función/propósito muy bien. ¿Qué 'ExcelHelper' se comunica? ¿Ayuda con qué ...? La dificultad con el nombre de la clase es uno de los signos de la violación de SRP: es más difícil encontrar el nombre apropiado para la clase. Definitivamente debería refactorizar parte de su código: su diseño será más limpio, más comprensible y más fácilmente comprobable. –

0

Una cosa es segura Estás haciendo TDD de la manera correcta :) Bueno, en el código anterior deberás burlarte del método GetDataTableFromExcelSheet antes de probar el método ExportExcelDocToDataSet.

Pero una cosa que puede hacer es pasar la tabla de datos en lugar de regresar de GetDataTableFromExcelSheet desde el lugar de su código en el que ha llamado método ExportExcelDocToDataSet agregando otro parámetro.

algo como esto

DataTable dtExcelData = GetData ....; y modificar el método de la siguiente manera

pública conjunto de datos ExportExcelDocToDataSet (bool headerRowProvided, DataTable dtExcelData)

De esta manera no tendrá que burlarse GetDataTableFromExcelSheet interior del método ExportExcelDocToDataSet al probar el método ExportExcelDocToDataSet.

+0

Un buen punto acerca de pasar DataTable en vez de solo obtener el valor de retorno. Tiendo a dar la vuelta sobre la mejor manera de hacerlo: pasar un objeto instanciado o devolverlo al método de llamada. Todavía tengo más dependencias más abajo en la pila de llamadas (no se muestra), así que todavía tendría el problema original con respecto a no poder probar algunos de esos métodos de nivel inferior. –

Cuestiones relacionadas