TotT: Contain Your Environment
October 30th, 2008 | Published in Google Testing
For example, consider a class that cleans the file system underlying a storage system:
// Deletes files that are no longer reachable via our storage system's
// metadata.
class FileCleaner {
public:
class Env {
public:
virtual bool MatchFiles(const char* pattern, vector* filenames) = 0;
virtual bool BulkDelete(const vector& filenames) = 0;
virtual MetadataReader* NewMetadataReader() = 0;
virtual ~Env();
};
// Constructs a FileCleaner. Uses “env” to access files and metadata.
FileCleaner(Env* env, QuotaManager* qm);
// Deletes files that are not reachable via metadata.
// Returns true on success.
bool CleanOnce();
};
FileCleaner::Env lets us test FileCleaner without accessing the real file system or metadata. It also makes it easy to simulate various kinds of failures, for example, of the file system:
class NoFileSystemEnv : public FileCleaner::Env {
virtual bool MatchFiles(const char* pattern, vector* filenames) {
match_files_called_ = true;
return false;
}
...
};
TEST(FileCleanerTest, FileCleaningFailsWhenFileSystemFails) {
NoFileSystemEnv* env = new NoFileSystemEnv();
FileCleaner cleaner(env, new MockQuotaManager());
ASSERT_FALSE(cleaner.CleanOnce());
ASSERT_TRUE(env->match_files_called_);
}
An Env object is particularly useful for restricting access to other modules or systems, for example, when those modules have overly-wide interfaces. This has the additional benefit of reducing your class's dependencies. However, be careful to keep the “real” Env implementation simple, lest you introduce hard-to-find bugs in the Env. The methods of your “real” Env implementation should just delegate to other, well-tested methods.
The most important benefits of an Env are that it documents how your class accesses its environment and it encourages future modifications to your module to keep tests small by extending and mocking out the Env.
Remember to download this episode of Testing on the Toilet and post it in your office.