2010-04-09 11 views
14

Todavía tengo problemas para justificar el TDD. Como ya he mencionado en otras cuestiones, el 90% del código que escribo no hace absolutamente nada más que¿Cómo puedo probar efectivamente contra la API de Windows?

  1. Call algunas funciones de API de Windows y
  2. imprimir los datos devueltos por dichas funciones.

el tiempo dedicado a dar con los datos falsos que el código necesita para procesar bajo TDD es increíble - Yo, literalmente, pasar 5 veces más tiempo dar con los datos de ejemplo que me gustaría pasar sólo escribir el código de aplicación.

Parte de este problema es que a menudo estoy programando contra API con las que tengo poca experiencia, lo que me obliga a escribir pequeñas aplicaciones que me muestran cómo se comporta la API real para poder escribir falsificaciones/burlas eficaces en la parte superior de esa API. Escribir implementación primero es lo opuesto a TDD, pero en este caso es inevitable: no sé cómo se comporta la API real, así que, ¿cómo voy a poder crear una implementación falsa de la API sin jugar con ella?

He leído varios libros sobre el tema, incluido el Test Driven Development de Kent Beck, Por ejemplo, y Michael Feathers 'Working Effectively with Legacy Code, que parece ser un evangelio para fanáticos de TDD. libro de plumas se acerca de la manera que se describe romper dependencias, pero aún así, los ejemplos proporcionados tienen una cosa en común:

  • El programa bajo prueba obtiene el aporte de otras partes del programa que se está probando.

Mis programas no siguen ese patrón. En cambio, la única entrada al programa en sí es el sistema sobre el que se ejecuta.

¿Cómo se puede emplear efectivamente TDD en un proyecto de este tipo? Ya estoy envolviendo la mayor parte de la API dentro de las clases de C++ antes de que realmente use esa API, pero a veces el wrappers themselves can become quite complicated, y merezco sus propias pruebas.

+1

Esto suena como un caso de "si la única herramienta que tengo es un martillo, cada problema parece un clavo". Quizás puede ignorar TDD cuando juega con la API y cuando obtiene algún tipo de sensación sobre cómo funciona, envuélvala en una interfaz y proporcione una implementación de maqueta para la API con fines de prueba y haga la TDD en la aplicación que usa la interfaz. . – Laserallan

+1

He estado en el mismo barco antes y no tengo una buena solución. Sin embargo, al menos incluso si primero escribe la implementación y luego escribe las pruebas en función de cómo se comporta la implementación (puede automatizar con la generación de código), está creando una red de seguridad que le avisará si cambia algo más adelante en la línea que rompe el comportamiento preexistente. Esto es realmente útil si tiene mucha reutilización de código, pero se encuentra teniendo que actualizar el código reutilizable para que se aplique a nuevos escenarios. Las pruebas preexistentes de la unidad asegurarán que no rompa nada con sus "mejoras". – AaronLS

+0

@Laserallan: Eso es lo que estoy haciendo. No tengo problemas para no usar TDD en los pequeños programas de prueba desechables. Tengo un problema al escribir toneladas de pequeñas aplicaciones de prueba desechables. –

Respuesta

13

Véase más abajo para FindFirstFile FindNextFile/ejemplo/FindClose


utilizo googlemock. Para una API externa, generalmente creo una clase de interfaz. Asumo que iba a llamar a fopen, fwrite, fclose

class FileIOInterface { 
public: 
    ~virtual FileIOInterface() {} 

    virtual FILE* Open(const char* filename, const char* mode) = 0; 
    virtual size_t Write(const void* data, size_t size, size_t num, FILE* file) = 0; 
    virtual int Close(FILE* file) = 0; 
}; 

La implementación real sería este

class FileIO : public FileIOInterface { 
public: 
    virtual FILE* Open(const char* filename, const char* mode) { 
    return fopen(filename, mode); 
    } 

    virtual size_t Write(const void* data, size_t size, size_t num, FILE* file) { 
    return fwrite(data, size, num, file); 
    } 

    virtual int Close(FILE* file) { 
    return fclose(file); 
    } 
}; 

Luego, utilizando googlemock hago una clase MockFileIO como esto

class MockFileIO : public FileIOInterface { 
public: 
    virtual ~MockFileIO() { } 

    MOCK_MEHTOD2(Open, FILE*(const char* filename, const char* mode)); 
    MOCK_METHOD4(Write, size_t(const void* data, size_t size, size_t num, FILE* file)); 
    MOCK_METHOD1(Close, int(FILE* file)); 
} 

Esto hace escribir las pruebas fácil. No tengo que proporcionar una implementación de prueba de Abrir/Escribir/Cerrar. googlemock maneja eso para mí. como en. (Nota que utilizo para mi googletest marco de pruebas de unidad.)

Supongamos que tengo una función como ésta, que necesita pruebas

// Writes a file, returns true on success. 
bool WriteFile(FileIOInterface fio, const char* filename, const void* data, size_size) { 
    FILE* file = fio.Open(filename, "wb"); 
    if (!file) { 
    return false; 
    } 

    if (fio.Write(data, 1, size, file) != size) { 
    return false; 
    } 

    if (fio.Close(file) != 0) { 
    return false; 
    } 

    return true; 
} 

Y aquí está la prueba.

TEST(WriteFileTest, SuccessWorks) { 
    MockFileIO fio; 

    static char data[] = "hello"; 
    const char* kName = "test"; 
    File test_file; 

    // Tell the mock to expect certain calls and what to 
    // return on those calls. 
    EXPECT_CALL(fio, Open(kName, "wb") 
     .WillOnce(Return(&test_file)); 
    EXPECT_CALL(fio, Write(&data, 1, sizeof(data), &test_file)) 
     .WillOnce(Return(sizeof(data))); 
    EXPECT_CALL(file, Close(&test_file)) 
     .WillOnce(Return(0)); 

    EXPECT_TRUE(WriteFile(kName, &data, sizeof(data)); 
} 

TEST(WriteFileTest, FailsIfOpenFails) { 
    MockFileIO fio; 

    static char data[] = "hello"; 
    const char* kName = "test"; 
    File test_file; 

    // Tell the mock to expect certain calls and what to 
    // return on those calls. 
    EXPECT_CALL(fio, Open(kName, "wb") 
     .WillOnce(Return(NULL)); 

    EXPECT_FALSE(WriteFile(kName, &data, sizeof(data)); 
} 

TEST(WriteFileTest, FailsIfWriteFails) { 
    MockFileIO fio; 

    static char data[] = "hello"; 
    const char* kName = "test"; 
    File test_file; 

    // Tell the mock to expect certain calls and what to 
    // return on those calls. 
    EXPECT_CALL(fio, Open(kName, "wb") 
     .WillOnce(Return(&test_file)); 
    EXPECT_CALL(fio, Write(&data, 1, sizeof(data), &test_file)) 
     .WillOnce(Return(0)); 

    EXPECT_FALSE(WriteFile(kName, &data, sizeof(data)); 
} 

TEST(WriteFileTest, FailsIfCloseFails) { 
    MockFileIO fio; 

    static char data[] = "hello"; 
    const char* kName = "test"; 
    File test_file; 

    // Tell the mock to expect certain calls and what to 
    // return on those calls. 
    EXPECT_CALL(fio, Open(kName, "wb") 
     .WillOnce(Return(&test_file)); 
    EXPECT_CALL(fio, Write(&data, 1, sizeof(data), &test_file)) 
     .WillOnce(Return(sizeof(data))); 
    EXPECT_CALL(file, Close(&test_file)) 
     .WillOnce(Return(EOF)); 

    EXPECT_FALSE(WriteFile(kName, &data, sizeof(data)); 
} 

No tuve que proporcionar una implementación de prueba de fopen/fwrite/fclose. googlemock maneja esto para mí. Puedes hacer el simulacro estricto si quieres. Una simulación estricta fallará las pruebas si se llama a alguna función que no se espera o si se llama a alguna función que se espera con los argumentos incorrectos. Googlemock proporciona una gran cantidad de ayudantes y adaptadores por lo que generalmente no es necesario escribir mucho código para que el simulacro haga lo que usted desea.Lleva unos días aprender los diferentes adaptadores, pero si lo usa a menudo se convierten en una segunda naturaleza.


Aquí hay un ejemplo usando FindFirstFile, FindNextFile, FindClose

En primer lugar la interfaz

class FindFileInterface { 
public: 
    virtual HANDLE FindFirstFile(
    LPCTSTR lpFileName, 
    LPWIN32_FIND_DATA lpFindFileData) = 0; 

    virtual BOOL FindNextFile(
    HANDLE hFindFile, 
    LPWIN32_FIND_DATA lpFindFileData) = 0; 

    virtual BOOL FindClose(
    HANDLE hFindFile) = 0; 

    virtual DWORD GetLastError(void) = 0; 
}; 

A continuación, la aplicación efectiva

class FindFileImpl : public FindFileInterface { 
public: 
    virtual HANDLE FindFirstFile(
    LPCTSTR lpFileName, 
    LPWIN32_FIND_DATA lpFindFileData) { 
    return ::FindFirstFile(lpFileName, lpFindFileData); 
    } 

    virtual BOOL FindNextFile(
    HANDLE hFindFile, 
    LPWIN32_FIND_DATA lpFindFileData) { 
    return ::FindNextFile(hFindFile, lpFindFileData); 
    } 

    virtual BOOL FindClose(
    HANDLE hFindFile) { 
    return ::FindClose(hFindFile); 
    } 

    virtual DWORD GetLastError(void) { 
    return ::GetLastError(); 
    } 
}; 

la maqueta usando gmock

class MockFindFile : public FindFileInterface { 
public: 
    MOCK_METHOD2(FindFirstFile, 
       HANDLE(LPCTSTR lpFileName, LPWIN32_FIND_DATA lpFindFileData)); 
    MOCK_METHOD2(FindNextFile, 
       BOOL(HANDLE hFindFile, LPWIN32_FIND_DATA lpFindFileData)); 
    MOCK_METHOD1(FindClose, BOOL(HANDLE hFindFile)); 
    MOCK_METHOD0(GetLastError, DWORD()); 
}; 

La función que necesito probar.

DWORD PrintListing(FindFileInterface* findFile, const TCHAR* path) { 
    WIN32_FIND_DATA ffd; 
    HANDLE hFind; 

    hFind = findFile->FindFirstFile(path, &ffd); 
    if (hFind == INVALID_HANDLE_VALUE) 
    { 
    printf ("FindFirstFile failed"); 
    return 0; 
    } 

    do { 
    if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { 
     _tprintf(TEXT(" %s <DIR>\n"), ffd.cFileName); 
    } else { 
     LARGE_INTEGER filesize; 
     filesize.LowPart = ffd.nFileSizeLow; 
     filesize.HighPart = ffd.nFileSizeHigh; 
     _tprintf(TEXT(" %s %ld bytes\n"), ffd.cFileName, filesize.QuadPart); 
    } 
    } while(findFile->FindNextFile(hFind, &ffd) != 0); 

    DWORD dwError = findFile->GetLastError(); 
    if (dwError != ERROR_NO_MORE_FILES) { 
    _tprintf(TEXT("error %d"), dwError); 
    } 

    findFile->FindClose(hFind); 
    return dwError; 
} 

La unidad prueba.

#include <gtest/gtest.h> 
#include <gmock/gmock.h> 

using ::testing::_; 
using ::testing::Return; 
using ::testing::DoAll; 
using ::testing::SetArgumentPointee; 

// Some data for unit tests. 
static WIN32_FIND_DATA File1 = { 
    FILE_ATTRIBUTE_NORMAL, // DWORD dwFileAttributes; 
    { 123, 0, },   // FILETIME ftCreationTime; 
    { 123, 0, },   // FILETIME ftLastAccessTime; 
    { 123, 0, },   // FILETIME ftLastWriteTime; 
    0,      // DWORD nFileSizeHigh; 
    123,     // DWORD nFileSizeLow; 
    0,      // DWORD dwReserved0; 
    0,      // DWORD dwReserved1; 
    { TEXT("foo.txt") }, // TCHAR cFileName[MAX_PATH]; 
    { TEXT("foo.txt") }, // TCHAR cAlternateFileName[14]; 
}; 

static WIN32_FIND_DATA Dir1 = { 
    FILE_ATTRIBUTE_DIRECTORY, // DWORD dwFileAttributes; 
    { 123, 0, },   // FILETIME ftCreationTime; 
    { 123, 0, },   // FILETIME ftLastAccessTime; 
    { 123, 0, },   // FILETIME ftLastWriteTime; 
    0,      // DWORD nFileSizeHigh; 
    123,     // DWORD nFileSizeLow; 
    0,      // DWORD dwReserved0; 
    0,      // DWORD dwReserved1; 
    { TEXT("foo.dir") }, // TCHAR cFileName[MAX_PATH]; 
    { TEXT("foo.dir") }, // TCHAR cAlternateFileName[14]; 
}; 

TEST(PrintListingTest, TwoFiles) { 
    const TCHAR* kPath = TEXT("c:\\*"); 
    const HANDLE kValidHandle = reinterpret_cast<HANDLE>(1234); 
    MockFindFile ff; 

    EXPECT_CALL(ff, FindFirstFile(kPath, _)) 
    .WillOnce(DoAll(SetArgumentPointee<1>(Dir1), 
        Return(kValidHandle))); 
    EXPECT_CALL(ff, FindNextFile(kValidHandle, _)) 
    .WillOnce(DoAll(SetArgumentPointee<1>(File1), 
        Return(TRUE))) 
    .WillOnce(Return(FALSE)); 
    EXPECT_CALL(ff, GetLastError()) 
    .WillOnce(Return(ERROR_NO_MORE_FILES)); 
    EXPECT_CALL(ff, FindClose(kValidHandle)); 

    PrintListing(&ff, kPath); 
} 

TEST(PrintListingTest, OneFile) { 
    const TCHAR* kPath = TEXT("c:\\*"); 
    const HANDLE kValidHandle = reinterpret_cast<HANDLE>(1234); 
    MockFindFile ff; 

    EXPECT_CALL(ff, FindFirstFile(kPath, _)) 
    .WillOnce(DoAll(SetArgumentPointee<1>(Dir1), 
        Return(kValidHandle))); 
    EXPECT_CALL(ff, FindNextFile(kValidHandle, _)) 
    .WillOnce(Return(FALSE)); 
    EXPECT_CALL(ff, GetLastError()) 
    .WillOnce(Return(ERROR_NO_MORE_FILES)); 
    EXPECT_CALL(ff, FindClose(kValidHandle)); 

    PrintListing(&ff, kPath); 
} 

TEST(PrintListingTest, ZeroFiles) { 
    const TCHAR* kPath = TEXT("c:\\*"); 
    MockFindFile ff; 

    EXPECT_CALL(ff, FindFirstFile(kPath, _)) 
    .WillOnce(Return(INVALID_HANDLE_VALUE)); 

    PrintListing(&ff, kPath); 
} 

TEST(PrintListingTest, Error) { 
    const TCHAR* kPath = TEXT("c:\\*"); 
    const HANDLE kValidHandle = reinterpret_cast<HANDLE>(1234); 
    MockFindFile ff; 

    EXPECT_CALL(ff, FindFirstFile(kPath, _)) 
    .WillOnce(DoAll(SetArgumentPointee<1>(Dir1), 
        Return(kValidHandle))); 
    EXPECT_CALL(ff, FindNextFile(kValidHandle, _)) 
    .WillOnce(Return(FALSE)); 
    EXPECT_CALL(ff, GetLastError()) 
    .WillOnce(Return(ERROR_ACCESS_DENIED)); 
    EXPECT_CALL(ff, FindClose(kValidHandle)); 

    PrintListing(&ff, kPath); 
} 

No tuve que implementar ninguna de las funciones simuladas.

+0

El problema no es configurar los talones. El problema es que Win32 devuelve estructuras de datos complicadas, y lleva mucho tiempo recuperar los datos de prueba para estas estructuras. A saber, porque no sé cómo se ve esa estructura de antemano. –

+2

Elija una función específica de Windows API para debatir. – gman

+0

¿Qué tal FindFirstFile/FindNextFile/FindClose? –

-1

Editar Entiendo que esto no es lo que necesita. Lo dejo aquí como wiki de la comunidad, ya que los comentarios son útiles.

Jaja, bueno, cada vez que veo anuncios de trabajo con las palabras: "requiere un desarrollo impulsado por pruebas" o "metodologías de desarrollo ágiles" y cosas por el estilo corro hacia el otro lado. Soy estrictamente de la opinión de que examinar el problema y comprender la mejor manera de resolverlo (trabajo en un par, o me relaciono regularmente con el cliente, o simplemente escribo algo en contra de las especificaciones de hardware) es parte del trabajo y no lo hace Necesito un nombre elegante y forzar proyectos que no los necesitan. Despotricar sobre.

Yo diría que no es necesario que, al menos, no necesite probar la API de Windows; está probando funciones para una API que no puede modificar de todos modos.

Si está creando una función que realiza algún proceso en el resultado de una llamada a la API de Windows, puede probarlo. Digamos, por ejemplo, que está tirando de Window Titles dado un hWnd e invirtiéndolos. No puede probar GetWindowTitle y SetWindowTitle, pero puede probar InvertString, que escribió, simplemente llamando a su función con "Thisisastring" y probando si el resultado de la función es "gnirtsasisihT". Si lo es, genial, actualice un puntaje de prueba en la matriz. Si no lo es, cariño, cualquier modificación que hayas hecho rompió el programa, no es bueno, vuelve y arregla.

Hay una pregunta sobre si es realmente necesario, para una función tan simple. ¿Tener una prueba previene que algún error se filtre? ¿Con qué frecuencia el algoritmo puede ser mal compilado/roto por cambios, etc.?

Dichas pruebas son más útiles en un proyecto en el que trabajo llamado MPIR, que compila contra muchas plataformas diferentes. Ejecutamos una compilación en cada una de estas plataformas y luego probamos el binario resultante para asegurarnos de que el compilador no haya creado un error a través de la optimización, o algo que hemos hecho al escribir el algoritmo no hace cosas inesperadas en esa plataforma. Es un cheque, para asegurarnos de no perdernos nada. Si pasa, genial, si falla, alguien va y mira por qué.

Personalmente, no estoy seguro de cómo un proceso de desarrollo completo puede ser impulsado exclusivamente por pruebas. Son cheques, después de todo. No te dicen cuándo es el momento de hacer un cambio significativo en la dirección en tu base de código, simplemente funciona lo que has hecho. Entonces, voy a ir tan lejos como para decir que TDD es solo una palabra de moda. Alguien se siente libre de estar en desacuerdo conmigo.

+0

No es TDD en sí lo que realmente estoy buscando. Son más las pruebas en sí mismas. Acabo de descubrir que demora aún más cuando diseño un sistema monolítico y luego trato de instrumentarlo para pruebas posteriores. Quizás escribí la pregunta mal. –

+0

Tiendo a construir cosas en pedazos. Esa es mi prueba. Entonces, si quiero obtener un título de ventana, construyo una función para hacerlo dado quizás un nombre parcial (o cualquier funcionalidad genérica que necesito) luego escribo un pequeño programa para verificar que funcione. Si lo hace, excelente, integre en el resto de la base de código. Esto dice, también puedo ejecutar herramientas como valgrind en ciertas partes del código. Creo que esto es exactamente lo que intentas evitar sin embargo? –

1

No creo que sea factible probar las clases de capas delgadas. Cuanto más grueso sea tu envoltorio, más fácil será probar los bits que no afectan directamente a la API, ya que el envoltorio puede tener múltiples capas, la más baja de las cuales puede ser burlada de alguna manera.

Mientras que usted podría hacer algo como:

// assuming Windows, sorry. 

namespace Wrapper 
{ 
    std::string GetComputerName() 
    { 
     char name[MAX_CNAME_OR_SOMETHING]; 
     ::GetComputerName(name); 
     return std::string(name); 
    } 
} 

TEST(GetComputerName) // UnitTest++ 
{ 
    CHECK_EQUAL(std::string(getenv("COMPUTERNAME")), Wrapper::GetComputerName()); 
} 

No sé que las pruebas de este tipo trae un montón de valor, y tenderían a hacer las pruebas de mi enfoque en la transformación de datos en lugar de la colección de tales.

+0

No es el paso burlón real que es difícil. Está creando datos de prueba complicados para realmente regresar del simulacro. –

+1

Correcto. No estaba tratando de demostrar un simulacro, sino dando un ejemplo de una prueba de una función y usar eso como un ejemplo de por qué creo que probar una API de terceros supuestamente bien probada no es una buena utilización del tiempo. –

+0

Hmmm ... el problema es que algunas de las envolturas no son exactamente delgadas. +1 por tomarse el tiempo para escribir una respuesta. –

Cuestiones relacionadas