如何正确地对从 FTP 服务器获取文件的 class 进行单元测试
How to properly unit test a class that gets file from an FTP server
其实我的问题分为两部分
- 如何将我的测试与外部隔离,但仍确保功能正常运行?
- 如何使用 Mockito 模拟 apache commons FtpClient class?当我嘲笑它时,我在以下位置得到空值:
InputStream inputStream =
ftpClient.retrieveFileStream(ftpParameters.getSourceFileName());
这是测试 class:
public class SimpleFtpFileImporterTest {
FtpParameters ftpParams =new FtpParameters();
SimpleFtpFileImporter fileImporter=new SimpleFtpFileImporter();
FTPClient ftpMock= mock(FTPClient.class);
@Before
public void startup(){
this.ftpParams.setServer("10.0.206.126");
this.ftpParams.setPort(21);
this.ftpParams.setUserName("mikola");
this.ftpParams.setPassword("password");
this.ftpParams.setSourceFileName("readme.txt");
}
@Test
public void returnNullWhenFileCouldNotBeFetchedCompletely() throws IOException{
when(ftpMock.completePendingCommand()).thenReturn(false);
fileImporter=new SimpleFtpFileImporter(ftpMock);
byte[] bytes= fileImporter.downloadFile(ftpParams);
assertNull(bytes);
}
}
这是被测系统:
public class SimpleFtpFileImporter implements IFileImporter {
private FTPClient ftpClient;
static Logger logger = Logger.getLogger(SimpleFtpFileImporter.class);
static {
PropertyConfigurator.configure("config/log4j.properties");
}
/**
* Creates a SimpleFtpFileImporter class instance passing an FtpClient.
* This constructor helps create unit tests by passing any kind of FTPClient, eg. an http ftp client.
*
* @param ftpClient An FTPClient object
*/
public SimpleFtpFileImporter(FTPClient ftpClient) {
this.ftpClient = ftpClient;
}
public SimpleFtpFileImporter() {
}
/**
* Gets the file specified from the specified FTP server
*
* @param ftpParameters An FtpParametrs object that bears the needed information
* @return File in byte array if successful, otherwise null
*/
public byte[] downloadFile(FtpParameters ftpParameters) {
if (this.ftpClient == null)
this.ftpClient = new FTPClient();
if (!ftpParameters.isProperlyPopulated()) {
logger.warn("Not all FTP parameters have been set. Execution will halt.");
throw new FtpParametersNotSetException("Ftp parameters not properly set.");
}
try {
ftpClient.connect(ftpParameters.getServer());
ftpClient.login(ftpParameters.getUserName(), ftpParameters.getPassword());
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
logger.info("FTP connection succesfully established. Preparing to retrieve file:"+ftpParameters.getSourceFileName());
InputStream inputStream = ftpClient.retrieveFileStream(ftpParameters.getSourceFileName());
if (inputStream != null) {
byte[] bytes = IOUtils.toByteArray(inputStream);
boolean success = ftpClient.completePendingCommand();
logger.info("File received");
inputStream.close();
if (success) {
return bytes;
} else{
logger.warn("File fetching process could not be through. Returning null.");
return null;
}
}else{
logger.warn("Wrong file name specified. File name:"+ftpParameters.getSourceFileName());
throw new RuntimeException("Wrong file name specified");
}
} catch (IOException ex) {
logger.error("Problem while trying to get file from remote FTP. Message: " + ex.getMessage() + " \n\r" + ex);
}
return null;
}
}
虽然我提供了所有需要的参数(主机、端口、用户名和密码),但模拟的 FtpClient 对象似乎不适合制作真实的东西
要回答到目前为止可以回答的部分 - 你必须学习如何编写 可测试 代码;一个好的起点是这些 videos.
你这边已经存在一个误区:模拟的FtpClient对象似乎不适合制作真实的东西。
没错。 mock 是一个 mock,一个空的 test stub。它没有做任何实际的事情。这就是使用模拟的全部意义所在。它们是空壳,除了提供您为它们指定的行为外什么都不做。
我的意思是:Mockito 创建的对象 不是 真正的 FtpClient。它只是一个 mock,"looks" 就像一个 FtpClient。它没有 任何 连接到 "real" FtpClient" class。换句话说:是的,您可以调用 FtpClient 具有的方法,但是他们都是空。他们什么都不做(好吧,他们做你指定他们做的事)。
重点是:您使用模拟来[=44=]将您与外部实现完全分离。通过给你的测试代码一个mock,你可以简单地忽略 "real" class正在做的一切。
查看您的代码和 "actual" 问题:
InputStream inputStream = ftpClient.retrieveFileStream(
为了让它工作,你必须配置你的mock为了return一些东西在调用 retrieveFileStream()
时很有用 !
我的意思是:你已经这样做了:
when(ftpMock.completePendingCommand()).thenReturn(false);
告诉 Mockito:当调用 completePendingCommand()
时,然后 return false
。您需要为被测代码调用的 每个 方法执行此操作!
除此之外;您的代码有很多问题,例如:
public SimpleFtpFileImporter() {
}
应该不为空;相反,你应该这样做:
public SimpleFtpFileImporter() {
this(new FtpClient());
}
那里的简单答案:默认情况下字段应该是 final(除非你有充分的理由不让它们成为 final):
private final FTPClient ftpClient;
编译器会告诉你忘记初始化那个字段了!在您的情况下,绝对 没有理由在构建时不初始化该字段。这只会让整个 class 变得更复杂,更难测试。
其实我的问题分为两部分
- 如何将我的测试与外部隔离,但仍确保功能正常运行?
- 如何使用 Mockito 模拟 apache commons FtpClient class?当我嘲笑它时,我在以下位置得到空值:
InputStream inputStream = ftpClient.retrieveFileStream(ftpParameters.getSourceFileName());
这是测试 class:
public class SimpleFtpFileImporterTest {
FtpParameters ftpParams =new FtpParameters();
SimpleFtpFileImporter fileImporter=new SimpleFtpFileImporter();
FTPClient ftpMock= mock(FTPClient.class);
@Before
public void startup(){
this.ftpParams.setServer("10.0.206.126");
this.ftpParams.setPort(21);
this.ftpParams.setUserName("mikola");
this.ftpParams.setPassword("password");
this.ftpParams.setSourceFileName("readme.txt");
}
@Test
public void returnNullWhenFileCouldNotBeFetchedCompletely() throws IOException{
when(ftpMock.completePendingCommand()).thenReturn(false);
fileImporter=new SimpleFtpFileImporter(ftpMock);
byte[] bytes= fileImporter.downloadFile(ftpParams);
assertNull(bytes);
}
}
这是被测系统:
public class SimpleFtpFileImporter implements IFileImporter {
private FTPClient ftpClient;
static Logger logger = Logger.getLogger(SimpleFtpFileImporter.class);
static {
PropertyConfigurator.configure("config/log4j.properties");
}
/**
* Creates a SimpleFtpFileImporter class instance passing an FtpClient.
* This constructor helps create unit tests by passing any kind of FTPClient, eg. an http ftp client.
*
* @param ftpClient An FTPClient object
*/
public SimpleFtpFileImporter(FTPClient ftpClient) {
this.ftpClient = ftpClient;
}
public SimpleFtpFileImporter() {
}
/**
* Gets the file specified from the specified FTP server
*
* @param ftpParameters An FtpParametrs object that bears the needed information
* @return File in byte array if successful, otherwise null
*/
public byte[] downloadFile(FtpParameters ftpParameters) {
if (this.ftpClient == null)
this.ftpClient = new FTPClient();
if (!ftpParameters.isProperlyPopulated()) {
logger.warn("Not all FTP parameters have been set. Execution will halt.");
throw new FtpParametersNotSetException("Ftp parameters not properly set.");
}
try {
ftpClient.connect(ftpParameters.getServer());
ftpClient.login(ftpParameters.getUserName(), ftpParameters.getPassword());
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
logger.info("FTP connection succesfully established. Preparing to retrieve file:"+ftpParameters.getSourceFileName());
InputStream inputStream = ftpClient.retrieveFileStream(ftpParameters.getSourceFileName());
if (inputStream != null) {
byte[] bytes = IOUtils.toByteArray(inputStream);
boolean success = ftpClient.completePendingCommand();
logger.info("File received");
inputStream.close();
if (success) {
return bytes;
} else{
logger.warn("File fetching process could not be through. Returning null.");
return null;
}
}else{
logger.warn("Wrong file name specified. File name:"+ftpParameters.getSourceFileName());
throw new RuntimeException("Wrong file name specified");
}
} catch (IOException ex) {
logger.error("Problem while trying to get file from remote FTP. Message: " + ex.getMessage() + " \n\r" + ex);
}
return null;
}
}
虽然我提供了所有需要的参数(主机、端口、用户名和密码),但模拟的 FtpClient 对象似乎不适合制作真实的东西
要回答到目前为止可以回答的部分 - 你必须学习如何编写 可测试 代码;一个好的起点是这些 videos.
你这边已经存在一个误区:模拟的FtpClient对象似乎不适合制作真实的东西。
没错。 mock 是一个 mock,一个空的 test stub。它没有做任何实际的事情。这就是使用模拟的全部意义所在。它们是空壳,除了提供您为它们指定的行为外什么都不做。 我的意思是:Mockito 创建的对象 不是 真正的 FtpClient。它只是一个 mock,"looks" 就像一个 FtpClient。它没有 任何 连接到 "real" FtpClient" class。换句话说:是的,您可以调用 FtpClient 具有的方法,但是他们都是空。他们什么都不做(好吧,他们做你指定他们做的事)。
重点是:您使用模拟来[=44=]将您与外部实现完全分离。通过给你的测试代码一个mock,你可以简单地忽略 "real" class正在做的一切。
查看您的代码和 "actual" 问题:
InputStream inputStream = ftpClient.retrieveFileStream(
为了让它工作,你必须配置你的mock为了return一些东西在调用 retrieveFileStream()
时很有用 !
我的意思是:你已经这样做了:
when(ftpMock.completePendingCommand()).thenReturn(false);
告诉 Mockito:当调用 completePendingCommand()
时,然后 return false
。您需要为被测代码调用的 每个 方法执行此操作!
除此之外;您的代码有很多问题,例如:
public SimpleFtpFileImporter() {
}
应该不为空;相反,你应该这样做:
public SimpleFtpFileImporter() {
this(new FtpClient());
}
那里的简单答案:默认情况下字段应该是 final(除非你有充分的理由不让它们成为 final):
private final FTPClient ftpClient;
编译器会告诉你忘记初始化那个字段了!在您的情况下,绝对 没有理由在构建时不初始化该字段。这只会让整个 class 变得更复杂,更难测试。