...
Code Block |
---|
package com.celestial.mockito.filetodb; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Filesstatic org.junit.jupiter.api.Assertions.assertEquals; import javaorg.niojunit.file.Paths; import java.util.ArrayList; import java.util.List; import org.junit.Test; import static org.junit.Assert.*;jupiter.api.Test; /** * * @author selvy */ public class LiveFileLoaderTest { // 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/kipstor-settings.js"; FileLoader cut = new FileLoader(fileToLoad); int expectedBytesRead = 301; // act int bytesRead = cut.loadFile(fileToLoad); // assert assertEquals(expectedBytesRead, bytesRead); } } |
...
Code Block |
---|
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 selvy */ public class FileLoader { String private class IntWrapper { public int value; } 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; } } |
...
FileLoaderTest - suitable for a build pipeline and does not break the tenets of unit testing
Code Block |
---|
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ 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 java.util.logging.Level; import java.util.logging.Logger; import org.junit.Test; import static org.junit.Assert.*; import org.mockito.MockedStatic; import org.mockito.Mockito; /** * * @author selvy */ public class FileLoaderTest { 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 java.util.logging.Level; import java.util.logging.Logger; import org.junit.Test; import static org.junit.Assert.*; import org.mockito.MockedStatic; import org.mockito.Mockito; /** * * @author selvy */ public class FileLoaderTest { // To use a different type of file system loader, pass a lambda to loadFile() // as shown here /* int bytesRead = cut.loadFile((fname) -> { List<String> result = null; try { result = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8); } catch (IOException e){} return result; }); */ // 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/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); ff.when(() -> Files.readAllLines(Paths.get(fileToLoad), StandardCharsets.UTF_8)).thenReturn(pretendFileContent); // 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); } // Redesign has worked so this test uses a stub to simulate a file being // loaded, it is passed in as lambda - We've decoupled ourselves from the // filesystem @Test public void load_all_of_file_via_stub() { // arrange String fileToLoad = "c:/tmp/KeyboardHandler.txt"; 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); } } |
FileLoader implementation code - refactored
Code Block | ||
---|---|---|
| ||
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
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 selvy
*/
public class FileLoader
{
class IntWrapper
{
int value;
}
String fileToLoad;
List<String> lines = Collections.emptyList();
public FileLoader(String fileToLoad)
{
this.fileToLoad = fileToLoad;
}
public List<String> getLines() {
return lines;
}
int loadFile(String fname)
{
try
{
lines = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8);
}
catch (IOException e)
{
}
return calculateFileSize();
}
int loadFile(ILoader func)
{
lines = func.loadFile(fileToLoad);
return calculateFileSize();
}
private int calculateFileSize()
{
IntWrapper result = new IntWrapper();
lines.forEach(line -> {
result.value += line.length();
});
return result.value;
}
} |
DBConnector
LiveDBConnectorTest - not suitable for a build server and breaks the tenets of a good unit test
Code Block | ||
---|---|---|
| ||
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.celestial.mockito.filetodb;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
/**
*
* @author selvy
*/
public class LiveDBConnectorTest
{
static String dbEndPoint = "jdbc:mysql://localhost:3306/";
static String dbName = "files";
// This test is not suitable for a build server. so we will need to stub
// IDbConnectionMgr
//@Ignore
@Test
public void open_connection() throws SQLException
{
// arrange
String dbEndPoint_ = "jdbc:mysql://localhost:3306/";
String dbName_ = "files";
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);
}
@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);
}
} |
DBConnector - Initial implementation
Code Block |
---|
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package com.celestial.mockito.filetodb;
import com.mysql.cj.xdevapi.PreparableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* @author selvy
*/
public class DBConnector
{
private IDbConnectionMgr connectionMgr;
private Connection theConnection;
private int fileId;
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;
}
Connection openConnection(String dbEndPoint, String dbName) throws SQLException
{
theConnection = connectionMgr.openConnection(dbEndPoint, dbName);
return theConnection;
}
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;
}
} |
DBConnectorTest - suitable for a build pipeline and does not break the tenets of unit testing
Code Block |
---|
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.celestial.mockito.filetodb; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import static org.junit.Assert.*; import org.junit.Test; import org.junit.BeforeClass; import org.junit.Ignore; import org.mockito.MockedStatic; import org.mockito.Mockito; import static org.mockito.Mockito.spy; /** * * @author selvy */ public class DBConnectorTest { static String dbEndPoint = "jdbc:mysql://localhost:3306/"; static String dbName = "files"; static Connection sqlConn = Mockito.mock(Connection.class); // Mockito does not like the line below to be present in each test method // it registers the static method with the current test thread once, if this line // is called more than once in a unit test file, a Mockito exception is thrown // // The live DB connection code has been moved to it's own test file because // it was pickup the mocked DriverManager and returning null because the // params being passed in (real values) did not match the stubbed values static MockedStatic<DriverManager> mockedDMgr = Mockito.mockStatic(DriverManager.class); @BeforeClass static public void beforeAllTestsDo() { // Because we are mocking the static, registration of the method should // only occur once for the unit test file mockedDMgr.when(() -> DriverManager.getConnection(dbEndPoint+dbName, "root", "ppp")).thenReturn(sqlConn); } @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(); // To use a different type of file system loader, pass a// lambdaact to loadFile() // as shown hereConnection conn = spyCut.openConnection(dbEndPoint, dbName); /* int bytesReadlineNoResult = cutspyCut.loadFile(getLineCount(fname); -> { // assert List<String> result = null; assertNotNull(conn); try assertEquals(expectedLineNoCount, lineNoResult); } { @Test public void write_line_to_mocked_connection() throws SQLException result = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8); { // }arrange IDbConnectionMgr connection catch= (IOExceptionendPoint, edb) -> {} return result;Connection conn = DriverManager.getConnection(endPoint+db, "root", "ppp"); }); */ return conn; // Redesign the FileLoader so that the machenism to load}; files up can be DBConnector //cut passed= in as a lambda - still titghtly coupled the file systemnew DBConnector(connection); @Test DBConnector publicspyCut void load_all_of_file_using_inbuilt_Files_type_as_lambda()= spy(cut); { Mockito.doReturn(0).when(spyCut).queryLinesInDB(); // arrange StringPreparedStatement fileToLoadps = "c:/tmp/KeyboardHandler.txt" Mockito.mock( PreparedStatement.class ); FileLoader cut = new FileLoader(fileToLoad);Mockito.when(sqlConn.prepareStatement(Mockito.any())).thenReturn(ps); int expectedBytesRead = 10; //1371spyCut.openConnection(dbEndPoint, dbName); List<String>String pretendFileContentlineToWrite = new ArrayList<>() "be not changed by..."; pretendFileContent.add("Hello") int expectedLineNo = 1; pretendFileContent.add("world"); MockedStatic<Files>// ffact = Mockito.mockStatic(Files.class); // ff.when(() -> Files.readAllLines(Paths.get(fileToLoad), StandardCharsets.UTF_8)).thenReturn(pretendFileContent); The idea of writeLine() is that achg time a line is written to the DB // actit's line No should be returned. Line Nos start intat bytesRead1 = cut.loadFile((fname) -> int lineNo {= spyCut.writeLine(lineToWrite); List<String> result = null; // assert try assertEquals(expectedLineNo, lineNo); } } |
DBConnector - refactored
Code Block |
---|
/* {* To change this license header, choose License Headers in Project Properties. * To change resultthis = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8); } catch (IOException e){} return result; }); // assert assertEquals(expectedBytesRead, bytesRead); } // Redesign has worked so this test uses a stub to simulate a file being // loaded, it is passed in as lambda - We've decoupled ourselves from the // filesystem @Test public void load_all_of_file_via_stub() { // arrange template file, choose Tools | Templates * and open the template in the editor. */ package com.celestial.mockito.filetodb; import com.mysql.cj.xdevapi.PreparableStatement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author selvy */ public class DBConnector { private final IDbConnectionMgr connectionMgr; private Connection theConnection; private int fileId = 1; 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; } Connection openConnection(String dbEndPoint, String dbName) throws SQLException { String fileToLoadtheConnection = "c:/tmp/KeyboardHandler.txt";connectionMgr.openConnection(dbEndPoint, dbName); FileLoader cut itsLineCount = new FileLoaderqueryLinesInDB(fileToLoad); int expectedBytesRead = 10; return theConnection; } public //int act getLineCount() { int bytesRead = cut.loadFile((fname) -> return itsLineCount; {} public void List<String> result = new ArrayList<>updateLineCount(); { updateLineCount(getLineCount() + 1); } result.add("Hello"); public void updateLineCount(int result.add("world");value) { itsLineCount return= resultvalue; } }); public int getFileId() { // assertreturn fileId; } assertEquals(expectedBytesRead, bytesRead); int queryLinesInDB() } { try } |
FileLoader implementation code - refactored
Code Block |
---|
/* * To change this license{ header, choose License Headers in Project Properties. * To change this templatePreparedStatement file,ps choose Tools | Templates * and open the template in the editor. */ 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 selvy */ public class FileLoader { class IntWrapper {= theConnection.prepareStatement(SQL_LINE_COUNT); ResultSet rs = ps.executeQuery(); while (rs.next()){ itsLineCount = rs.getInt(1); int value;} } String fileToLoad; List<String> lines} =catch Collections.emptyList(SQLException ex); public FileLoader(String fileToLoad){ { this.fileToLoad = fileToLoadLogger.getLogger(DBConnector.class.getName()).log(Level.SEVERE, null, ex); } public List<String> getLines() {} return linesitsLineCount; } public int loadFilewriteLine(String fnamelineToWrite) { { int fileId try= getFileId(); try { { lines = Files.readAllLines(Paths.get(fname), StandardCharsets.UTF_8); PreparedStatement ps = theConnection.prepareStatement(SQL_INSERT_LINE); } catch ps.setInt(IOException1, efileId); { ps.setString(2, lineToWrite); } return calculateFileSizeps.executeUpdate(); } int loadFile(ILoader func) updateLineCount(); { lines = func.loadFile(fileToLoad); } returncatch calculateFileSize(SQLException ex); } { private int calculateFileSize() Logger.getLogger(DBConnector.class.getName()).log(Level.SEVERE, null, ex); { } IntWrapper result = newreturn IntWrappergetLineCount(); } } |
Interfaces
Code Block |
---|
/* * To change this license header, choose License Headers in lines.forEach(line -> { result.value += line.length(); }); Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package com.celestial.mockito.filetodb; import java.util.List; /** * * @author selvy */ @FunctionalInterface public interface ILoader { List<String> return result.value; } } loadFile(String fname); } |