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 36 Current »

I’ve been writing software since I was 12 years old and I’m at a healthy age now lol. I’ve been developing software professionally for 37 years (meaning I’ve been getting paid for it). I love it and hope to be coding till I die (smile) Unit testing was introduced to me some 15 years ago, but I never really grasped its significance till about 2010 when I was introduced to JUnit. I soon realised I’d got a new way of controlling my little hacks when I was trying to test something that I’d written. So rather than having several static pubic void main() method, each with its own series of print statements and an ad-hoc collection of if statements to control what I was trying to test and print out, I could organise things into tidy little functions each focused on testing one thing. And so the journey into unit testing and TDD began.

With this wonderful toolset and the philosophy of TDD, there is an approach that is missing. What do you do when are faced with a problem that is foreign to you and have very little understanding of the API required to solve the problem? Pre-unit test and TDD I would begin by hacking some code together and using static public void main() as the testing ground, with the code growing ever more complex with each piece of new understanding. But during the COVID lockdown across the world, I decided to enter a new phase of learning (every day is a school day lol). I wanted to follow a TDD approach to learning how to use an API and at the same time develop the solution.

In this talk, I will be using Java and make a number of assumptions - you know what a test double is, and how to write a JUnit test case. What I say here is equally applicable if you are a C++, C# or another language developer.

Test doubles are an integral part of your testing toolkit. There are five types of test doubles that we commonly speak about, Dummy, Fake, Mock, Spy, Stub.

In this section, I will focus on Stubs, Mocks, and Spies.

It is a commonly held view that a Dummy, Fake, Spy, and Stub are Mocks. They are not. Each one is a type-of Test Double with a very specific purpose.

A Mock and Spy can be categorized as interaction Test Doubles - they focus on peering on the inside of CUT to ensure that it behaves as expected and that the CUT interacts with the Mock and Spy as expected.

There are lots of Mocking frameworks out there, I am using Mockito in my discussion.

So what’s the problem we’re trying to solve?

To answer this, we need to ask another question. What are the basic tenets of a unit test?

  1. Test one method in a class

  2. Not be an integration test

  3. Execute quickly

  4. Be independent of other unit tests, in other words, isolated

  5. Does not interact with a non-in-memory database

  6. Does not perform any file IO

  7. Does not perform any network IO

  8. The scope of the test should be between the Unit Test, the CUT, and any periphery objects to the CUT, but not its grand children

Points 5, 6, and 7 are of important interest here. Unit tests are ultimately deployed to a build server. In doing so there are no guarantees that tests that exhibit these capabilities will pass because the database will not be accessible, the file system will more likely be blocked, and networking will be restricted if not totally blocked.

Let’s write some code

So here is the problem we will walk through and discuss - an application that reads a text file and stores the contents of that file in a database. Each line in the text file is stored as a row in a database table. The schema for the database will be

The testing environment will look like this

So my initial thinking was to have three classes

  • FileToDbLoader - performs the transfer of data from a file object of some kind

  • FileLoader - loads the data from the file

  • DbConnector - opens a connection to the DB, and performs the writes to the DB

You might be wondering how I was able to come up with these three classes so quickly. I’ve been working in an OO world since 1988 (some of the first implementations of C++ and modeling software with OMT, Booch, then UML). I can read a requirement and very quickly perform a noun analysis and determine the required classes. Using TDD helped refine that thinking.

We are going to be using already tested APIs such java.nio.Files and java.sql.DriverManager. So we are not testing these calls. We are using these APIs in our code and want to be sure that our code performs as expected.

I began my design on the FileLoader by writing an initial test

package com.celestial.mockito.filetodb;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author selvyn
 */
public class FileLoaderTest 
{
    // The inital design is described in this test
    /*
     The weakness should be obvious?  The file to be loaded and it's location
     I use a shared network drive to run this code from different machines, normally
     dev-ing from a PC.  When I ran the code on the laptop from a cafe it 
     immediately failed because the C: on the laptop was completely different
     to the PC, so the original file C:/tmp/KeyboardHandler.txt did not exist
    
     THIS IS A GREAT EXAMPLE OF WHY THE UNIT TEST AND CUT SHOULD NOT BE STRONGLY
     LINKED TO ANY IO - NETWORK, DB, AND FILE SYSTEM
    */
     @Test
     public void load_all_of_file_using_inbuilt_Files_type() 
     {
         // arrange
         String fileToLoad = "c:/tmp/KeyboardHandler.txt";
         FileLoader cut = new FileLoader(fileToLoad);
         int expectedBytesRead = 301;
         
         // act
         int bytesRead = cut.loadFile(fileToLoad);
         
         // assert
         assertEquals(expectedBytesRead, bytesRead);
     }
}

It soon became apparent that this test would fail.

And the CUT - the issue is at line 28, tight coupling to the static method

package com.celestial.mockito.filetodb;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;


/**
 *
 * @author selvyn
 */
public class FileLoader 
{
    String fileToLoad;
    List<String> lines = Collections.emptyList();

    public FileLoader(String fileToLoad) 
    {
        this.fileToLoad = fileToLoad;
    }

    int loadFile(String fname) 
    {
        try
        {
            lines = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8);
        }
        catch (IOException e){}

        return calculateFileSize();
    }    
    
    private int calculateFileSize()
    {
        IntWrapper result = new IntWrapper();
        
        lines.forEach(line -> {
                result.value += line.length();
        });
        
        return result.value;
    }
}

The failure comes from this simple fact. The code is stored on a network drive so I can access it and develop it from a number of machines. Each of these machines has a C: drive but their contents are very different. So initially the test worked, but when I moved to work on one of the laptops it failed because the file in the test did not exist. So yes I could have stored the test text file within the project, but if I had done so, the problem would never have been revealed, So the test failed one of the tenets of good unit tests; no File IO.

For what I was trying to do I liked the java.nio.Files class. The coupling between FileLoader and Files is high, but at some point, there is going to be some level of dependency in your code. You need to decide what is an acceptable level. Unit tests are great for revealing if you got that level of dependency correct - the testability test.

My initial thought was to use constructor injection. Inject the Files class (not the object since we need the static methods on the class) into the FileLoader, but this was wrought with complications that I don’t want to discuss here and would lead us away from the main discussion. So the approach I have chosen is to pass an anonymous object into the FileLoader via the loadFile() method. The loadFile() method requires the field member “lines” to have been set before calculateFileSize() is called.

The implementor of the anonymous object can then add whatever functionality they want but it must return an instance of a List<string>. In other words, they can stub the required Files behaviour. This is a very old technique from the world of C++ programming called a functor (function object).

Interestingly, as well as following a TDD approach, I’m using the unit test as a place for discovery and experimentation given that I was unfamiliar with java.nio.Files. Also notice that I am using a stub not a Mock. The use of Stubs and Mocks is not exclusive of each other.

So I have a new test because I maintain the story of the evolution in my thinking

     // Redesign the FileLoader so that the machenism to load files up can be 
     // passed in as a lambda - still titghtly coupled the file system
     @Test
     public void load_all_of_file_using_inbuilt_Files_type_as_lambda() 
     {
         // arrange
         String fileToLoad = "c:/tmp/kipstor-settings.js";
         FileLoader cut = new FileLoader(fileToLoad);
         int expectedBytesRead = 301;
         
         // act
         int bytesRead = cut.loadFile((fname) ->
         {
            List<String> result = null;
            try
            {
                result = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8);
            }
            catch (IOException e){}
            return result;
         });
         
         // assert
         assertEquals(expectedBytesRead, bytesRead);
     }

To support this, we will create a functional interface

package com.celestial.mockito.filetodb;

import java.util.List;

/**
 *
 * @author selvyn
 */
@FunctionalInterface
public interface ILoader 
{
    List<String>    loadFile(String fname);
}

And add a new method to FileLoader that can take this anonymous object

    int loadFile(ILoader func) 
    {
        lines = func.loadFile(fileToLoad);
        return calculateFileSize();
    }    

So I’m using the functor (lambda) to decouple the FileLoader from java.nio.Files.

This test works but it’s still constrained because it is trying to access the local file system. So, let’s stub out the functor with some dummy data so that loadFile() has something to work with other than the local file system

A new test

     @Test
     public void load_all_of_file_via_stub() 
     {
         // arrange
         String fileToLoad = "c:/tmp/KeyboardHandler.txt";  // not needed for the test
         FileLoader cut = new FileLoader(fileToLoad);
         int expectedBytesRead = 10;
         
         // act
         int bytesRead = cut.loadFile((fname) ->
         {
            List<String> result = new ArrayList<>();
            
            result.add("Hello");
            result.add("world");

            return result;
         });
         
         // assert
         assertEquals(expectedBytesRead, bytesRead);
     }

So this is really interesting. Using unit tests and a TDD approach has lead to some pretty interesting design decisions. I’m not sure if I would have reached the same design if I had not used unit tests and TDD, where the tenets of a good unit test constrained me.

To support our overall design aim, we need one more method, a getter to retrieve the List<string> of lines from the FileLoader.

Using the functor approach and Mockito to create a stub for the Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8); call we can rewrite the load_all_of_file_using_inbuilt_Files_type_as_lambda() test to

     @Test
     public void load_all_of_file_using_inbuilt_Files_type_as_lambda() 
     {
         // arrange
         String fileToLoad = "c:/tmp/KeyboardHandler.txt";
         FileLoader cut = new FileLoader(fileToLoad);
         int expectedBytesRead = 10;    //1371;
         List<String> pretendFileContent = new ArrayList<>();
         pretendFileContent.add("Hello");
         pretendFileContent.add("world");
         MockedStatic<Files> ff = Mockito.mockStatic(Files.class);  // mocking a class for its static methods
         ff.when(() -> Files.readAllLines(Paths.get(fileToLoad), StandardCharsets.UTF_8)).thenReturn(pretendFileContent); // mocking the static method
         
         // act
         int bytesRead = cut.loadFile((fname) ->
         {
            List<String> result = null;
            try
            {
                result = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8);
            }
            catch (IOException e){}
            return result;
         });
         
         // assert
         assertEquals(expectedBytesRead, bytesRead);
     }

Now we have code that can be deployed onto the build server and describes how to use the FileLoader

We are now ready to work on the DBConnector

As I began to work on the test for this class it soon became apparent that was going to have a similar problem as with the FileLoader. The java.sql.DriverManager.getConnection(…) is a static method. So again I set about creating a functional interface so that I could create a functor as and when I need one. Again the functor will be used to decouple java.sql.DriverManager from our implementation code. This was my initial test

     @Test
     public void initial_open_connection() throws SQLException 
     {
         // arrange
         String dbEndPoint = "jdbc:mysql://localhost:3306/";
         String dbName = "files";
         
         // act
         Connection conn = DriverManager.getConnection(dbEndPoint+dbName, "root", "ppp");
         
         // assert
         assertNotNull(conn);
     }

What I needed was this (yes I know bad security around the user name and password - not the point of the discussion). Notice the use of the functor again (lines 7-10)?

     @Test
     public void open_connection() throws SQLException 
     {
         // arrange
         String dbEndPoint = "jdbc:mysql://localhost:3306/";
         String dbName = "files";
         IDbConnectionMgr dbConnection = (endPoint, db) -> {
             Connection conn = DriverManager.getConnection(endPoint+db, "root", "ppp");
             return conn;
         };
         DBConnector cut = new DBConnector(dbConnection);
         
         // act
         Connection conn = cut.openConnection(dbEndPoint, dbName);
         
         // assert
         assertNotNull(conn);
     }

Did I write this test completely then work on the implementation code? NO

I’m finding that using a TDD approach allows me to develop the code in an evolutionary manner. Given that I knew I wanted to wrap the factory class and method DriverManager.getConnection(…) I created this first

String dbEndPoint = "jdbc:mysql://localhost:3306/"; String dbName = "files"; IDbConnectionMgr dbConnection = (endPoint, db) -> { Connection conn = DriverManager.getConnection(dbEndPoint+dbName, "root", "ppp"); return conn; }; DBConnector cut = new DBConnector(dbConnection);

The above allowed me to create the functional interface

@FunctionalInterface
public interface IDbConnectionMgr
{
    Connection    openConnection(String endPoint, String dbName) throws SQLException;   
}

Originally openConnection(…) was a void operation, but then I realised I couldn’t test if the connection had succeeded. Also, without the Connection, the DBConnector would not be able to perform any DB requests.

So the implementation class currently looks like this

public class DBConnector
{
    private IDbConnectionMgr connectionMgr;
    private Connection theConnection;
    
    DBConnector(IDbConnectionMgr connection)
    {
        connectionMgr = connection;
    }

    Connection  openConnection(String dbEndPoint, String dbName) throws SQLException
    {
        theConnection = connectionMgr.openConnection(dbEndPoint, dbName);
        
        return theConnection;
    }
}

Now we are ready to redesign the test so that it’s deployable on a build server

     @Test
     public void open_connection_mocked() throws SQLException 
     {
         // arrange
         String dbEndPoint = "jdbc:mysql://localhost:3307/";
         String dbName = "files";
         Connection sqlConn = Mockito.mock(Connection.class);
         MockedStatic<DriverManager> mockedDMgr = Mockito.mockStatic(DriverManager.class);
         mockedDMgr.when(() -> DriverManager.getConnection(dbEndPoint+dbName, "root", "ppp")).thenReturn(sqlConn);

         IDbConnectionMgr connection = (endPoint, db) -> {
             Connection conn = DriverManager.getConnection(endPoint+db, "root", "ppp");
             return conn;
         };
         DBConnector cut = new DBConnector(connection);
         
         // act
         Connection conn = cut.openConnection(dbEndPoint, dbName);
         
         // assert
         assertNotNull(conn);
     }

This test works just as expected. The CUT is working as designed. Changing the port number as shown in line 5 does not cause the test to fail because the test is isolated from the network and physical DB. And the cut is not dependent on that value

NOTE ABOUT MOCKITO AND MOCKING STATICS

Do NOT use the mockStatic on the same class or method across different unit test. You will get a strange mockito error when the test suite runs. When you want to use the same static mock for a class or methods, declare them as attributes of the unit test.

All of the above reveals some interesting observations when using unit tests and TDD

  1. When you are working on an API you’ve never seen before, you must go through a period of experimentation and discovery. This will mean the tests are unstable and will need refactoring.

  2. The discovery period holds valuable insights into how an API works. The discovery period will usually involve working with components that break the tenets of a good unit test, such as network access, file IO, DB access, etc. I would not throw these tests away because they capture through documentation insights that I have discovered. I would simply move these to another test file called live tests. I would also annotate these tests with @Ignore so they do not trigger a build failure on the build pipeline.

Writing a line to the database

My thinking here is to have a method that takes the string to be written to the DB and returns its line number/row in the DB.

So here is the test

     @Test
     public void write_line_to_live_connection() throws SQLException 
     {
         // arrange
         IDbConnectionMgr connection = (endPoint, db) -> {
             Connection conn = DriverManager.getConnection(endPoint+db, "root", "ppp");
             return conn;
         };
         DBConnector cut = new DBConnector(connection);
         cut.openConnection(dbEndPoint, dbName);
         String lineToWrite = "be not changed by...";
         int expectedLineNo = 1;
         
         // act
         // The idea of writeLine() is that each time a line is written to the DB
         // it's line No should be returned.  Line Nos start at 1
         int lineNo = cut.writeLine(lineToWrite);
         
         // assert
         assertEquals(expectedLineNo, lineNo);
     }

And here is the initial implementation in DBConnector{ … }

    private static final String SQL_INSERT_LINE = "insert into files.line_in_file" +
         " values (default, ?, ?);";
    private static final String SQL_LINE_COUNT = "select count(*) from files.line_in_file;";

    int writeLine(String lineToWrite)
    {
        int result = 0;
        fileId = 1;
        try
        {
            PreparedStatement ps = theConnection.prepareStatement(SQL_INSERT_LINE);
            ps.setInt(1, fileId);
            ps.setString(2, lineToWrite);
            ps.executeUpdate();
            
            ps = theConnection.prepareStatement(SQL_LINE_COUNT);

            ResultSet rs = ps.executeQuery();
            while (rs.next()){
                result = rs.getInt(1);
            }
            
        } catch (SQLException ex)
        {
            Logger.getLogger(DBConnector.class.getName()).log(Level.SEVERE, null, ex);
        }
        return result;
    }

This does result in an interesting design decision that must be made - running a SQL query after each insert would give us the row id/line #, but be very slow. A more optimised approach would be to query the last row # when we connect to the DB and store this in the DBConnector. So we are going to have to modify the openConnection() method so that it queries the DB for the row count.

So here is the redesign of the writeLine()

    int writeLine(String lineToWrite)
    {
        fileId = 1;
        try
        {
            PreparedStatement ps = theConnection.prepareStatement(SQL_INSERT_LINE);
            ps.setInt(1, fileId);
            ps.setString(2, lineToWrite);
            ps.executeUpdate();
            
            updateLineCount();
            
        } catch (SQLException ex)
        {
            Logger.getLogger(DBConnector.class.getName()).log(Level.SEVERE, null, ex);
        }
        return getLineCount();
    }

The DBConnector class fields and constructor look like this

public class DBConnector
{
    private final IDbConnectionMgr connectionMgr;
    private Connection theConnection;
    private int fileId;
    private int itsLineCount;
    private static final String SQL_INSERT_LINE = "insert into files.line_in_file" +
         " values (default, ?, ?);";
    private static final String SQL_LINE_COUNT = "select count(*) from files.line_in_file;";
    
    DBConnector(IDbConnectionMgr connection)
    {
        connectionMgr = connection;
    }

In keeping with not breaking the tests, openConnection() has been updated to

    Connection  openConnection(String dbEndPoint, String dbName) throws SQLException
    {
        theConnection = connectionMgr.openConnection(dbEndPoint, dbName);
        
        itsLineCount = queryLinesInDB();
        
        return theConnection;
    }

You will notice two new methods updateLineCount() and queryLinesInDB()

    public  int     getLineCount()
    {
        return itsLineCount;
    }
    
    public  void    updateLineCount()
    {
        updateLineCount(getLineCount() + 1);
    }
    
    public  void    updateLineCount(int value)
    {
        itsLineCount = value;
    }

    int queryLinesInDB()
    {
        fileId = 1;
        try
        {
            PreparedStatement ps = theConnection.prepareStatement(SQL_LINE_COUNT);

            ResultSet rs = ps.executeQuery();
            while (rs.next()){
                itsLineCount = rs.getInt(1);
            }
            
        } catch (SQLException ex)
        {
            Logger.getLogger(DBConnector.class.getName()).log(Level.SEVERE, null, ex);
        }
        return itsLineCount;
    }

The tests haven’t greatly changed and they still pass, which is a great and good demonstration of following the TDD rules.

Now we are ready to translate the write_line_to_live_connection() test method so that it can be deployed on a build server. Before we do this, I want to re-examine the open_connection() test method. Now that we have bought line number into the equation, and it is retrieved when the DB connection opened, we can add this to the test result - so that a valid DB connection would be that we connect to the database and are able to retrieve the current line number

     @Test
     public void open_connection_mocked() throws SQLException 
     {
         // arrange
         IDbConnectionMgr connection = (endPoint, db) -> {
             Connection conn = DriverManager.getConnection(endPoint+db, "root", "ppp");
             return conn;
         };
         DBConnector cut = new DBConnector(connection);
         DBConnector spyCut = spy(cut);
         int expectedLineNoCount = 1;
         Mockito.doReturn(1).when(spyCut).queryLinesInDB();
         
         // act
         Connection conn = spyCut.openConnection(dbEndPoint, dbName);
         int lineNoResult = spyCut.getLineCount();
         
         // assert
         assertNotNull(conn);
         assertEquals(expectedLineNoCount, lineNoResult);
     }

Notice on line 12 I’m expecting a call the queryLinesInDB() if the connection was successful and no exception is thrown. We can now test the DBConnector’s state by reading this field value @line 16.

Spies are an excellent tool for overriding behaviour on the CUT. You cannot mock the CUT because this is the thing you are trying to test, but you can spy on it.

So here is a little rule to remember for when to use use mocks and spies - “mock the children and spy on the parent”; the CUT == parent, and CUTs dependants == children

We are now able to mock out the write_line_to_mocked_connection() test method

     @Test
     public void write_line_to_mocked_connection() throws SQLException 
     {
         // arrange
         Connection sqlConn = Mockito.mock(Connection.class);
         MockedStatic<DriverManager> mockedDMgr = Mockito.mockStatic(DriverManager.class);
         mockedDMgr.when(() -> DriverManager.getConnection(dbEndPoint+dbName, "root", "ppp")).thenReturn(sqlConn);
         IDbConnectionMgr connection = (endPoint, db) -> {
             Connection conn = DriverManager.getConnection(endPoint+db, "root", "ppp");
             return conn;
         };
         DBConnector cut = new DBConnector(connection);
         
         DBConnector spyCut = spy(cut);
         Mockito.doReturn(0).when(spyCut).queryLinesInDB();
         
         PreparedStatement ps = Mockito.mock( PreparedStatement.class );
         Mockito.when(sqlConn.prepareStatement(Mockito.any())).thenReturn(ps);
         
         spyCut.openConnection(dbEndPoint, dbName);
         String lineToWrite = "be not changed by...";
         int expectedLineNo = 1;
         
         // act
         // The idea of writeLine() is that achg time a line is written to the DB
         // it's line No should be returned.  Line Nos start at 1
         int lineNo = spyCut.writeLine(lineToWrite);
         
         // assert
         assertEquals(expectedLineNo, lineNo);
     }

  • No labels