One of the goals of writeSnippet is to send the total seconds to the descriptor. In the prior section, we inspected this value by turning it into a member variable. We can instead have the WavReader use a test double of the descriptor that captures the total seconds sent to it.
You learned about how to create test doubles using Google Mock in Chapter 5, Test Doubles. Since we’re using CppUTest for our current example, we’ll use its own mock tool, CppUMock. As with Google Mock, we define a derivative of WavDescriptor that will spy on messages sent to its add function.
wav/15/WavReaderTest.cpp | |
| class MockWavDescriptor : public WavDescriptor { |
| public: |
| MockWavDescriptor(): WavDescriptor("") {} |
| void add( |
| const string&, const string&, |
| uint32_t totalSeconds, |
| uint32_t, uint32_t) override { |
* | mock().actualCall("add") |
* | .withParameter("totalSeconds", (int)totalSeconds); |
| } |
| }; |
The override to add requires us to make it virtual in WavDescriptor.
wav/15/WavDescriptor.h | |
* | virtual void add( |
* | const std::string& dir, const std::string& filename, |
* | uint32_t totalSeconds, uint32_t samplesPerSecond, |
* | uint32_t channels) { |
* | // ... |
| WavDescriptorRecord rec; |
| cpy(rec.filename, filename.c_str()); |
| rec.seconds = totalSeconds; |
| rec.samplesPerSecond = samplesPerSecond; |
| rec.channels = channels; |
| |
| outstr->write(reinterpret_cast<char*>(&rec), sizeof(WavDescriptorRecord)); |
| } |
The highlighted line in MockWavDescriptor tells a global CppUTest MockSupport object (retrieved by a call to mock) to record an actual call to a function named “add.” The MockSupport object also captures the value of a parameter named “totalSeconds.” (I quote these names since you get to choose them arbitrarily when you work with CppUMock. It’s a cheap form of reflection.)
We inject the test double into the WavReader by passing it as a third argument to its constructor.
wav/15/WavReaderTest.cpp | |
| TEST_GROUP(WavReader_WriteSnippet) { |
* | shared_ptr<MockWavDescriptor> descriptor{new MockWavDescriptor}; |
* | WavReader reader{"", "", descriptor}; |
| istringstream input{""}; |
| FormatSubchunk formatSubchunk; |
| ostringstream output; |
| DataChunk dataChunk; |
| char* data; |
| uint32_t TwoBytesWorthOfBits{2 * 8}; |
| void setup() override { |
| data = new char[4]; |
| } |
| |
| void teardown() override { |
| mock().clear(); |
| delete[] data; |
| } |
| }; |
In the test itself, we tell the MockSupport object to expect that a function with name add gets called. We tell it to expect that the call is made with a specific value for its parameter named totalSeconds. This arrangement of the test is known as setting an expectation. Once the actual call to writeSnippet gets made, the Assert portion of the test verifies that all expectations added to the MockSupport object were met.
wav/15/WavReaderTest.cpp | |
| TEST(WavReader_WriteSnippet, UpdatesTotalSeconds) { |
| dataChunk.length = 8; |
| formatSubchunk.bitsPerSample = TwoBytesWorthOfBits; |
| formatSubchunk.samplesPerSecond = 1; |
* | mock().expectOneCall("add").withParameter("totalSeconds", 8 / 2 / 1); |
| reader.writeSnippet("any", input, output, formatSubchunk, dataChunk, data); |
* | mock().checkExpectations(); |
| } |
We correspondingly change the descriptor pointer member to be a shared pointer. Using a shared pointer allows both the test and the production code to properly manage creating and deleting the descriptor object. We also choose to default the descriptor argument to be a null pointer in order to minimize the impact to existing tests.
wav/15/WavReader.h | |
| class WavReader { |
| public: |
| WavReader( |
| const std::string& source, |
| const std::string& dest, |
| std::shared_ptr<WavDescriptor> descriptor=0); |
| // ... |
| private: |
| // ... |
| std::shared_ptr<WavDescriptor> descriptor_; |
| }; |
wav/15/WavReader.cpp | |
| WavReader::WavReader( |
| const std::string& source, |
| const std::string& dest, |
* | shared_ptr<WavDescriptor> descriptor) |
| : source_(source) |
| , dest_(dest) |
* | , descriptor_(descriptor) { |
* | if (!descriptor_) |
* | descriptor_ = make_shared<WavDescriptor>(dest); |
| |
| channel = DEF_CHANNEL("info/wav", Log_Debug); |
| log.subscribeTo((RLogNode*)RLOG_CHANNEL("info/wav")); |
| |
| rLog(channel, "reading from %s writing to %s", source.c_str(), dest.c_str()); |
| } |
| |
| WavReader::~WavReader() { |
* | descriptor_.reset(); |
| delete channel; |
| } |
We don’t have to change a lick of code in the writeSnippet function that we’re testing! Code in writeSnippet blissfully continues to call the add function on the descriptor without knowing whether the descriptor is a production WavDescriptor instance or a test double.
We can finally complete the story by test-driving that writeSnippet obtains and passes on the file size. In fact, we choose to co-opt the test UpdatesTotalSeconds and update it to verify both arguments. We create a mock for FileUtil in order to support answering a stub value given a request for a file size. We inject the FileUtil mock instance of FileUtil using a setter function instead of the constructor.
wav/16/WavReaderTest.cpp | |
| class MockWavDescriptor : public WavDescriptor { |
| public: |
| MockWavDescriptor(): WavDescriptor("") {} |
| void add( |
| const string&, const string&, |
| uint32_t totalSeconds, |
| uint32_t, uint32_t, |
| uint32_t fileSize) override { |
| mock().actualCall("add") |
| .withParameter("totalSeconds", (int)totalSeconds) |
* | .withParameter("fileSize", (int)fileSize); |
| } |
| }; |
| |
* | class MockFileUtil: public FileUtil { |
* | public: |
* | streamsize size(const string& name) override { |
* | return mock().actualCall("size").returnValue().getIntValue(); |
* | } |
* | }; |
| |
| TEST_GROUP(WavReader_WriteSnippet) { |
| shared_ptr<MockWavDescriptor> descriptor{new MockWavDescriptor}; |
| WavReader reader{"", "", descriptor}; |
| |
* | shared_ptr<MockFileUtil> fileUtil{make_shared<MockFileUtil>()}; |
| |
| istringstream input{""}; |
| FormatSubchunk formatSubchunk; |
| ostringstream output; |
| DataChunk dataChunk; |
| char* data; |
| uint32_t TwoBytesWorthOfBits{2 * 8}; |
| |
| const int ArbitraryFileSize{5}; |
| |
| void setup() override { |
| data = new char[4]; |
* | reader.useFileUtil(fileUtil); |
| } |
| |
| void teardown() override { |
| mock().clear(); |
| delete[] data; |
| } |
| }; |
| |
| TEST(WavReader_WriteSnippet, SendsFileLengthAndTotalSecondsToDescriptor) { |
| dataChunk.length = 8; |
| formatSubchunk.bitsPerSample = TwoBytesWorthOfBits; |
| formatSubchunk.samplesPerSecond = 1; |
| |
* | mock().expectOneCall("size").andReturnValue(ArbitraryFileSize); |
| |
| mock().expectOneCall("add") |
| .withParameter("totalSeconds", 8 / 2 / 1) |
| |
* | .withParameter("fileSize", ArbitraryFileSize); |
| |
| reader.writeSnippet("any", input, output, formatSubchunk, dataChunk, data); |
| |
| mock().checkExpectations(); |
| } |
wav/16/WavReader.cpp | |
| void WavReader::writeSnippet( |
| const string& name, istream& file, ostream& out, |
| FormatSubchunk& formatSubchunk, |
| DataChunk& dataChunk, |
| char* data |
| ) { |
| // ... |
| writeSamples(&out, data, startingSample, samplesToWrite, bytesPerSample); |
| |
| rLog(channel, "completed writing %s", name.c_str()); |
| |
* | auto fileSize = fileUtil_->size(name); |
| |
| descriptor_->add(dest_, name, |
| totalSeconds, formatSubchunk.samplesPerSecond, formatSubchunk.channels, |
* | fileSize); |
| |
| //out.close(); // ostreams are RAII |
| } |
52.14.82.217