Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 3 Next »

We are going examine the benefits of using TDD as a design technique and see what the benfits are if we follow a TDD approach to developing production code.

Phase 1

Write the unit test first for a CUT called FileLoader.  It should only have one public method on it.  Once the test is written develop the CUT to pass the test.

public int LoadFile(string fname)

The above function should load a text file from the disk, and return the number or characters in it.

Your test should specify the input the file name including its path, and the number of characters expected be in the file.

Assert on the actual file length (number of characters in the file) against the expected file length (number of characters read from the file)

In the production class we suggest you use something like File.ReadLines()

Once you have completed the above test and production code, think about this question

  • Question: What’s the weakness of this design

A solution can be found here

Phase 2

Write the unit test first for a CUT called FileLoader.  It should only have a new public method on it.  Once the test is written develop the CUT to pass the test.

In order to facilitate your design, you need to think about the structure of the test

  1. In the //Act part of the test, we want to write something like this

        // act
        int bytesRead = cut.LoadFile(fileToLoad, (fname) =>
        {
            Array<string> result = null;
            try
            {
                result = File.ReadLines(fname);
            }
            catch (IOException e) { }
            return result;
        });

2. Reading the API guides we see that File.ReadLines() returns a IEnumerable<string>, so let’s change the code to match this

        // act
        int bytesRead = cut.LoadFile(fileToLoad, (fname) =>
        {
            IEnumerable<string> result = null;
            try
            {
                result = File.ReadLines(fname);
            }
            catch (IOException e) { }
            return result;
        });

3. So we need to design a LoadFile() method that takes in two parameters, the file name, and a block of code that can read the file from any source.

public int LoadFile(string fname, ILoadFile func)

You will need to declare ILoadFile as follows

public delegate IEnumerable<string> ILoadFile(String fname);

4. Modify FileLoader so it has this additional functionality

namespace file_loader_service
{
    public delegate IEnumerable<string> ILoadFile(String fname);

    public class FileLoader
    {
        IEnumerable<string> lines = new List<string>();

        public int LoadFile(string fname, ILoadFile func)
        {
            lines = func(fname);
            return CalculateFileSize();
        }
...

By moving the mechanics of how a file is loaded into FileLoader we’ve decoupled the functionality. This new design now means we can instruct FileLoader to load a file resource in different ways, not just using the standard file IO.

So the test has forced us to think carefully about what we think is the purpose of FileLoader. Our initial intent may have been to load a file from the disk, which is can do, but we have broadened it’s design so it is more flexible and less brittle.

Once you have completed the above test and production code, think about this question

  • Question: What’s the weakness of this design

A solution can be found here

Phase 3 - working stubs

The phase 2 test was weakened by the fact that It broke one of the tenants o a good test

  • A test should not make any IO calls

Because we have designed the FileLoader so that a lambda can be passed in, we can rethink what the test should look like.

Create a new test that specifically targets using canned data.

In the //Act part of the test redesign the lambda expression so that it returns a list of strings.

You should not have to change the CUT.

Once you have completed the above test and production code, think about this question

  • Question: Can we further improve the test

A solution can be found here

Phase 4 - working with mocks

Mocks are not stubs

A Mock can be used to create Stubs, and Dummies

A mocking library will create a dummy object that must be populated with methods that have predefined results and parameters

In this implementation of the test System.IO.File object. But wait, System.IO.File.ReadLines() is a class-level operation and not an object-level operation. Most mocking frameworks struggle with mocking class-level methods.

We are going to use a mocking library called NSubstitute. It’s one of the more popular mocking libraries for .Net code.

Begin by writing your test in the normal way

        // arrange
        string fileToLoad = "c:/tmp/KeyboardHandler.java.txt";
        FileLoader cut = new FileLoader();
        // setup ur canned data, these will represent the lines in the file
        List<string> pretendFileContent = new();
        pretendFileContent.Add("Hello");
        pretendFileContent.Add("world");
        int expectedBytesRead = 10;

Because System.IO.File cannot be mocked because it is a static class, we will create our own dummy interface with the required methods for our tests and CUT

namespace file_loader_tests
{
    public  interface MyFile
    {
        public abstract IEnumerable<string> ReadLines(string path);
    }
}

In our test, we can now mock this interface and set the expectations for the required method

        // Mock the interface
        var file = Substitute.For<MyFile>();
        
        // Setup the expectation
        file.ReadLines(fileToLoad).Returns(pretendFileContent);

Line 2 creates a skeletal object of the MyFile interface.

Line 5 tells the mocked object that when the method ReadLines() is invoked the parameter value given in fileToRead, it should return the object pretendFileContent. We will look at this more closely in a short while.

Now in the //Act part of the test add the following code

        // act
        int bytesRead = cut.LoadFile(fileToLoad, (fname) =>
        {
            IEnumerable<string> result = file.ReadLines(fileToLoad);

            return result;
        });

In the lambda’s body, we simply invoke file.ReadLines(fileToLoad). This is using the mocked object and mocked method we previously set up.

The Assert statement should be no different from the previous Assert statements in the other tests.

Run the test.

A solution can be found here

Do expectation parameters really matter?

Let’s look at the code

            var file = Substitute.For<MyFile>();
            file.ReadLines(fileToLoad).Returns(pretendFileContent);

            // act
            int bytesRead = cut.LoadFile(fileToLoad, (fname) =>
            {
                IEnumerable<string> result = file.ReadLines(fileToLoad);

                return result;
            });

Change line 7 to

                IEnumerable<string> result = file.ReadLines("xyz");

Rerun the test and see what happens. It should fail.

When you set up an expectation on a mock object, you are not just specifying what method will be called, you are stating what values will be passed in as parameters to that method, and what value should be returned if those values match what you have said. In other words, if when the test is run the test values match the expected values, then the value you have stated will be returned. This is why it is called setting up the expectations.

  • No labels