A
Andrew
Hi,
I am thinking about ways for efficient techniques for isolation testing.
Here is the problem as I see it:
Complex OO systems are designed with massive number of dependencies between
modules and classes. In most cases these dependencies end-up in external
systems such as databases and OS. Overhead in designing systems with "could
be stubbed" in mind is great, so in many cases dropped.
I would appreciate if anyone interested in this problem could share his/her
opinion on possible approaches for isolation testing. I also attached some
code with my approach to it. It is based on introducing template parameter
with explicit template instantiation and specification.
Some comments to example and code:
Existing system:
- ImageDB class responsible for reading / writing images to database.
- Image class responsible for retrieval of required image from database,
processing it and storing back to database and depends on ImageDB class.
- Both classes are in middle of developed, many function are not yet fully
implemented.
Task:
- Create test unit for processing functionality (Image::clear() method)
without overhead related to preparing and populating DB with test data.
- Calls to Image and ImageDB classes should keep the same syntax.
Files (sorry for bulky example):
Before (code without stubbing facilities):
======================
// file: app/main.cpp
#include "image.h"
int main( )
{
Image img;
img.load( "John Doe" );
img.clear( );
return 0;
}
----------------------
// file: app/image.cpp
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <string>
#include "../services/imagedb.h"
class Image
{
public:
Image( );
~Image( );
void load( const std::string& name );
void clear( );
private:
void deallocate( );
ImageDB imageDB_;
size_t width_;
size_t height_;
unsigned int* buffer_;
bool isLoaded_;
std::string name_;
};
#endif // __IMAGE_H__
----------------------
// file: app/image.h
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <string>
#include "../services/imagedb.h"
class Image
{
public:
Image( );
~Image( );
void load( const std::string& name );
void clear( );
private:
void deallocate( );
ImageDB imageDB_;
size_t width_;
size_t height_;
unsigned int* buffer_;
bool isLoaded_;
std::string name_;
};
#endif // __IMAGE_H__
------------------------------
// services/imagedb.h
#ifndef __IMAGEDB_H__
#define __IMAGEDB_H__
#include <string>
class ImageDB
{
public:
ImageDB( );
bool getImageByName( const std::string& name, unsigned int*& buffer,
size_t& width, size_t& height );
bool setImageByName( const std::string& name, unsigned int* buffer,
size_t width, size_t height );
};
#endif // __IMAGEDB_H__
------------------------------
// services/imagedb.cpp
#include "imagedb.h"
ImageDB::ImageDB( ) {
throw 1; // cannot connect to database
}
bool ImageDB::getImageByName( const std::string& name,
unsigned int*& buffer, size_t& width,
size_t& height ) {
return false; // not implemented yet
}
bool ImageDB::setImageByName( const std::string& name,
unsigned int* buffer, size_t width,
size_t height ) {
return false; // not implemented yet
}
===========================
After (with some stubbing facilities and implemented unit test):
===========================
// file: utest/utest.cpp
#include <string>
struct TestImageProc { };
#define __DIAGNOSTICS__ TestImageProc
#define private public
#include "../app/image.h"
#undef private
// Stubbing ImageDB ctor, so it doesn't throws exception
template< > ImageDBT< TestImageProc >::ImageDBT( ) {}
// Stubbing ImageDB functionality, so it now returns 'true'
template< > bool ImageDBT< TestImageProc >::setImageByName(
const std::string& s, unsigned int* b,
size_t w, size_t h ) {
return true;
};
// Stubbing Image constructor, so we do not 'delete [ ] buffer_'
template< > ImageT< TestImageProc >::~ImageT( ) { };
int main( ) {
// test inputs, test setup
Image img;
img.width_ = img.height_ = 2;
unsigned int buf[ ] = { 1, 1, 1, 1 };
img.buffer_ = buf;
// test
img.clear( );
if( buf[ 0 ] != 0 )
{
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
---------------
// file: services/imagedb.h
#ifndef __IMAGEDB_H__
#define __IMAGEDB_H__
#include <string>
template< typename Diagnostics >
class ImageDBT {
public:
ImageDBT( );
bool getImageByName( const std::string& name, unsigned int*& buffer,
size_t& width, size_t& height );
bool setImageByName( const std::string& name, unsigned int* buffer,
size_t width, size_t height );
};
#ifndef __DIAGNOSTICS__
typedef ImageDBT< void > ImageDB;
#else
typedef ImageDBT< __DIAGNOSTICS__ > ImageDB;
#include "imagedb.cpp"
#endif
#endif // __IMAGEDB_H__
---------------
// file: imagedb.cpp
#include "imagedb.h"
#ifndef __DIAGNOSTICS__
template class ImageDBT< void >;
#endif
template< typename D >
ImageDBT< D >::ImageDBT( ) {
throw 1; // not yet defined
}
template< typename D >
bool ImageDBT< D >::getImageByName( const std::string& name, unsigned int*&
buffer,
size_t& width, size_t& height ) {
return false; // not yet implemented, fails
}
template< typename D >
bool ImageDBT< D >::setImageByName( const std::string& name, unsigned int*
buffer,
size_t width, size_t height ) {
return false; // not yet implemented, fails
}
---------
// file: app/main.cpp
#include "image.h"
int main( ) {
Image img;
img.load( "John Doe" );
img.clear( );
return 0;
}
-----------------------
// file: app/image.h
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <string>
#include "../services/imagedb.h"
template< typename Diagnostics >
class ImageT {
public:
ImageT( );
~ImageT( );
void load( const std::string& name );
void clear( );
private:
void deallocate( );
ImageDB imageDB_;
size_t width_;
size_t height_;
unsigned int* buffer_;
bool isLoaded_;
std::string name_;
};
#ifndef __DIAGNOSTICS__
typedef ImageT< void > Image;
#else
typedef ImageT< __DIAGNOSTICS__ > Image;
#include "image.cpp"
#endif
#endif //__IMAGE_H__
-----------------
// file: app/image.cpp
#include "image.h"
#include <stdexcept>
#ifndef __DIAGNOSTICS__
template class ImageT< void >; // explicite instanciation of version without
stubbing
#endif
template< typename D > ImageT< D >::ImageT( ):
width_( 0 ), height_( 0 ), buffer_( NULL )
{ }
template< typename D > ImageT< D >::~ImageT( ) {
deallocate( );
}
template< typename D > void ImageT< D >::load( const std::string& name ) {
deallocate( );
isLoaded_ = imageDB_.getImageByName( name, buffer_, width_, height_ );
name_ = name;
}
template< typename D > void ImageT< D >::clear( ) {
if( isLoaded_ ) {
unsigned int* end = buffer_ + width_ * height_;
for( unsigned int* p = buffer_; p != end; ++p ) {
*p = 0;
}
imageDB_.setImageByName( name_, buffer_, width_, height_ );
}
}
template< typename D > void ImageT< D >::deallocate( ){
delete [ ] buffer_;
buffer_ = NULL;
width_ = height_ = 0;
isLoaded_ = false;
name_.empty( );
}
I am thinking about ways for efficient techniques for isolation testing.
Here is the problem as I see it:
Complex OO systems are designed with massive number of dependencies between
modules and classes. In most cases these dependencies end-up in external
systems such as databases and OS. Overhead in designing systems with "could
be stubbed" in mind is great, so in many cases dropped.
I would appreciate if anyone interested in this problem could share his/her
opinion on possible approaches for isolation testing. I also attached some
code with my approach to it. It is based on introducing template parameter
with explicit template instantiation and specification.
Some comments to example and code:
Existing system:
- ImageDB class responsible for reading / writing images to database.
- Image class responsible for retrieval of required image from database,
processing it and storing back to database and depends on ImageDB class.
- Both classes are in middle of developed, many function are not yet fully
implemented.
Task:
- Create test unit for processing functionality (Image::clear() method)
without overhead related to preparing and populating DB with test data.
- Calls to Image and ImageDB classes should keep the same syntax.
Files (sorry for bulky example):
Before (code without stubbing facilities):
======================
// file: app/main.cpp
#include "image.h"
int main( )
{
Image img;
img.load( "John Doe" );
img.clear( );
return 0;
}
----------------------
// file: app/image.cpp
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <string>
#include "../services/imagedb.h"
class Image
{
public:
Image( );
~Image( );
void load( const std::string& name );
void clear( );
private:
void deallocate( );
ImageDB imageDB_;
size_t width_;
size_t height_;
unsigned int* buffer_;
bool isLoaded_;
std::string name_;
};
#endif // __IMAGE_H__
----------------------
// file: app/image.h
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <string>
#include "../services/imagedb.h"
class Image
{
public:
Image( );
~Image( );
void load( const std::string& name );
void clear( );
private:
void deallocate( );
ImageDB imageDB_;
size_t width_;
size_t height_;
unsigned int* buffer_;
bool isLoaded_;
std::string name_;
};
#endif // __IMAGE_H__
------------------------------
// services/imagedb.h
#ifndef __IMAGEDB_H__
#define __IMAGEDB_H__
#include <string>
class ImageDB
{
public:
ImageDB( );
bool getImageByName( const std::string& name, unsigned int*& buffer,
size_t& width, size_t& height );
bool setImageByName( const std::string& name, unsigned int* buffer,
size_t width, size_t height );
};
#endif // __IMAGEDB_H__
------------------------------
// services/imagedb.cpp
#include "imagedb.h"
ImageDB::ImageDB( ) {
throw 1; // cannot connect to database
}
bool ImageDB::getImageByName( const std::string& name,
unsigned int*& buffer, size_t& width,
size_t& height ) {
return false; // not implemented yet
}
bool ImageDB::setImageByName( const std::string& name,
unsigned int* buffer, size_t width,
size_t height ) {
return false; // not implemented yet
}
===========================
After (with some stubbing facilities and implemented unit test):
===========================
// file: utest/utest.cpp
#include <string>
struct TestImageProc { };
#define __DIAGNOSTICS__ TestImageProc
#define private public
#include "../app/image.h"
#undef private
// Stubbing ImageDB ctor, so it doesn't throws exception
template< > ImageDBT< TestImageProc >::ImageDBT( ) {}
// Stubbing ImageDB functionality, so it now returns 'true'
template< > bool ImageDBT< TestImageProc >::setImageByName(
const std::string& s, unsigned int* b,
size_t w, size_t h ) {
return true;
};
// Stubbing Image constructor, so we do not 'delete [ ] buffer_'
template< > ImageT< TestImageProc >::~ImageT( ) { };
int main( ) {
// test inputs, test setup
Image img;
img.width_ = img.height_ = 2;
unsigned int buf[ ] = { 1, 1, 1, 1 };
img.buffer_ = buf;
// test
img.clear( );
if( buf[ 0 ] != 0 )
{
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
---------------
// file: services/imagedb.h
#ifndef __IMAGEDB_H__
#define __IMAGEDB_H__
#include <string>
template< typename Diagnostics >
class ImageDBT {
public:
ImageDBT( );
bool getImageByName( const std::string& name, unsigned int*& buffer,
size_t& width, size_t& height );
bool setImageByName( const std::string& name, unsigned int* buffer,
size_t width, size_t height );
};
#ifndef __DIAGNOSTICS__
typedef ImageDBT< void > ImageDB;
#else
typedef ImageDBT< __DIAGNOSTICS__ > ImageDB;
#include "imagedb.cpp"
#endif
#endif // __IMAGEDB_H__
---------------
// file: imagedb.cpp
#include "imagedb.h"
#ifndef __DIAGNOSTICS__
template class ImageDBT< void >;
#endif
template< typename D >
ImageDBT< D >::ImageDBT( ) {
throw 1; // not yet defined
}
template< typename D >
bool ImageDBT< D >::getImageByName( const std::string& name, unsigned int*&
buffer,
size_t& width, size_t& height ) {
return false; // not yet implemented, fails
}
template< typename D >
bool ImageDBT< D >::setImageByName( const std::string& name, unsigned int*
buffer,
size_t width, size_t height ) {
return false; // not yet implemented, fails
}
---------
// file: app/main.cpp
#include "image.h"
int main( ) {
Image img;
img.load( "John Doe" );
img.clear( );
return 0;
}
-----------------------
// file: app/image.h
#ifndef __IMAGE_H__
#define __IMAGE_H__
#include <string>
#include "../services/imagedb.h"
template< typename Diagnostics >
class ImageT {
public:
ImageT( );
~ImageT( );
void load( const std::string& name );
void clear( );
private:
void deallocate( );
ImageDB imageDB_;
size_t width_;
size_t height_;
unsigned int* buffer_;
bool isLoaded_;
std::string name_;
};
#ifndef __DIAGNOSTICS__
typedef ImageT< void > Image;
#else
typedef ImageT< __DIAGNOSTICS__ > Image;
#include "image.cpp"
#endif
#endif //__IMAGE_H__
-----------------
// file: app/image.cpp
#include "image.h"
#include <stdexcept>
#ifndef __DIAGNOSTICS__
template class ImageT< void >; // explicite instanciation of version without
stubbing
#endif
template< typename D > ImageT< D >::ImageT( ):
width_( 0 ), height_( 0 ), buffer_( NULL )
{ }
template< typename D > ImageT< D >::~ImageT( ) {
deallocate( );
}
template< typename D > void ImageT< D >::load( const std::string& name ) {
deallocate( );
isLoaded_ = imageDB_.getImageByName( name, buffer_, width_, height_ );
name_ = name;
}
template< typename D > void ImageT< D >::clear( ) {
if( isLoaded_ ) {
unsigned int* end = buffer_ + width_ * height_;
for( unsigned int* p = buffer_; p != end; ++p ) {
*p = 0;
}
imageDB_.setImageByName( name_, buffer_, width_, height_ );
}
}
template< typename D > void ImageT< D >::deallocate( ){
delete [ ] buffer_;
buffer_ = NULL;
width_ = height_ = 0;
isLoaded_ = false;
name_.empty( );
}