c++ winapi tdd

api de windows c++



¿Cómo puedo probar efectivamente contra la API de Windows? (3)

Todavía tengo problemas para justificar el TDD. Como mencioné en otras preguntas, el 90% del código que escribo no hace absolutamente nada más que

  1. Llamar a algunas funciones API de Windows y
  2. Imprima los datos devueltos de dichas funciones.

El tiempo dedicado a generar los datos falsos que el código necesita procesar en TDD es increíble. Literalmente, gasto 5 veces más el tiempo que me queda con los datos de ejemplo que me gustaría dedicar a escribir el código de la 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 efectivas encima 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 el trabajo de Michael Feathers Working Effectively with Legacy, que parece ser un evangelio para los fanáticos de TDD. El libro de Plumas se acerca en la forma en que describe las dependencias de salida, pero aun así, los ejemplos proporcionados tienen una cosa en común:

  • El programa bajo prueba obtiene información de otras partes del programa bajo prueba.

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 las envolturas mismas pueden volverse bastante complicadas y merecen sus propias pruebas.


No creo que sea factible probar las clases de envoltura delgada. 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 puedes 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é si las pruebas como esta aportan mucho valor, y tienden a hacer que mi prueba se centre en la transformación de los datos en lugar de en la recopilación de los mismos.


Editar . Entiendo que esto no es lo que necesitas. 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 basado en 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, al menos, no es necesario que pruebes la API de Windows; estás probando funciones para una API que no puedes modificar de todos modos.

Si está creando una función que realiza algún proceso en el resultado de una llamada API de Windows, puede probar eso. 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 eso 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.?

Estas pruebas son más útiles en un proyecto en el que trabajo llamado MPIR, que se basa en 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.


Consulte a continuación el archivo FindFirstFile / FindNextFile / FindClose

Yo uso googlemock . Para una API externa, generalmente creo una clase de interfaz. Supongamos 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 esta

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, con googlemock hago una clase MockFileIO como esta

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 que escribir las pruebas sea fácil. No tengo que proporcionar una implementación de prueba de Abrir / Escribir / Cerrar. googlemock maneja eso para mí. como en. (nota que uso googletest para mi marco de prueba de unidad.)

Supongamos que tengo una función como esta 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án las pruebas.

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. Se necesitan algunos días para aprender los diferentes adaptadores, pero si lo usa con frecuencia se convierten rápidamente en una segunda naturaleza.

Aquí hay un ejemplo con FindFirstFile, FindNextFile, FindClose

Primero 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; };

Entonces la implementación real

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(); } };

El simulacro 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.