如何测试这个文件写入,3lines 功能?
How to test this file-writing, 3lines function?
这是我在某些服务中的方法class。它是 public 所以它应该被测试。我根本不知道我应该测试什么。我会模拟 Writer
和 spyOn 函数调用,但使用此实现是不可能的(不是吗?)
我正在使用 Mockito
和 JUnit
现在,我只能创建函数来抛出并断言该异常
有什么帮助吗?
@Override
public void initIndexFile(File emptyIndexFile) {
try {
Writer writer = new FileWriter(emptyIndexFile);
writer.write("[]");
writer.close();
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
为了测试您的方法是否可以通过发送正确的命令与编写器正确交互,您的程序必须公开某种 "seam" 以便您的测试可以配置模拟 FileWriter
。我不熟悉 mockito
但一种方法是将 FileWriter
实例化封装在方法后面,然后您的测试可以将该方法重写为 return 模拟 FileWriter
.
假设File
是一个接口:
public Writer getFileWriter(File emptyIndexFile) {
return new FileWriter(emptyIndexFile);
}
这可以让您覆盖上述方法进行测试和 return 伪造 Writer
@Override
public Writer getFileWriter(File emptyIndexFile) {
return mockFileWriterInstance;
}
然后您的测试可以进行练习 initIndexFile
并对操作进行断言。使用模拟文件编写器应该很容易抛出 IOException
以便您可以练习错误处理逻辑。
您可以简单地在测试中为您的方法提供一个临时文件,然后简单地检查它是否包含预期的 []
,一旦超过就删除该文件。
类似于:
public class FileWritingTest {
// File to provide to the method initIndexFile
private File file;
/* This is executed before the test */
@Before
public void init() throws IOException {
// Create a temporary file
this.file = File.createTempFile("FileWritingTest", "tmp");
// Indicates that it should be removed on exit
file.deleteOnExit();
}
/* This is executed after the test */
@After
public void clean() throws IOException {
// Delete the file once test over
file.delete();
}
@Test
public void testInitIndexFile() throws IOException {
FileWriting fw = new FileWriting();
// Call the method
fw.initIndexFile(this.file);
// Check that the content is [] as expected
Assert.assertEquals("[]", new String(Files.readAllBytes(file.toPath())));
}
}
注意 1: 我依赖 new String(byte[])
这意味着我依赖默认字符编码,就像您在当前代码中所做的那样,但这不是一个好习惯, 我们应该明确设置字符编码以避免平台依赖。
注意2:假设你使用java7或更高版本,你应该考虑使用try-with-resources 声明正确关闭你的作者,你的代码将是:
public void initIndexFile(File emptyIndexFile) {
try (Writer writer = new FileWriter(emptyIndexFile)) {
writer.write("[]");
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
模拟依赖项是可能的,也是自然的,但是模拟在方法主体中声明的对象是不自然且棘手的。
我设想了 3 种解决方案:
1) 为什么不简单地断言文件是用预期的字符写入的,而不是模拟?
它避免了技巧,但如果您经常执行此任务并且想要对其进行单元测试,它可能会多余且缓慢。
2) 使局部变量成为一个实例字段来模拟它。这似乎真的不是一个干净的解决方案。如果您在执行此类处理的同一个 class 中有多个方法,则可能会重复使用同一个编写器或具有多个编写器字段。在这两种情况下,您都可能会产生副作用。
3) 如果你执行很多写操作并且你想真正隔离对编写器的调用,你有一个解决方案:重新设计你的代码以获得可测试的 class.
您可以提取依赖项来执行编写器处理。 class 可以提供一种带有执行指令所需参数的方法。我们可以称它为:WriteService
。
public class WriteService {
...
public void writeAndClose(Writer writer, String message){
try {
writer.write(message);
writer.close();
}
catch (IOException e) {
throw new IndexFileInitializationException("Error initialization index file " + emptyIndexFile.getPath());
}
}
}
这个 class 是可测试的,因为 writer 依赖是一个参数。
你这样调用新服务:
public class YourAppClass{
private WriteService writeService;
public YourAppClass(WriteService writeService){
this.writeService=writeService;
}
@Override
public void initIndexFile(File emptyIndexFile) {
Writer writer = new FileWriter(emptyIndexFile);
writeService.writeAndClose(writer,"[]");
}
}
现在 initIndexFile()
也可以通过模拟 WriteService
进行测试。
您可以检查 writeAndClose() 是在 writeService 上使用 good 参数调用的。
就我个人而言,我会使用第一种解决方案或第三种解决方案。
如果您认为添加特殊内容是业务逻辑,因此是您class的责任,然后创建 FileWriter 不是(根据 单一责任模式。
因此您应该使用在测试 下注入您的Class 的FileWriterFactory
。然后你可以模拟 FileWriterFactory
到 return Writer
接口的模拟实现,然后你可以检查它是否得到了预期的字符串。
你的 CuT 会变成这样:
private final WriterFactory writerFactory;
public ClassUnderTest(@Inject WriterFactory writerFactory){
this.writerFactory = writerFactory;
}
@Override
public void initIndexFile(File emptyIndexFile) {
try {
Writer writer = writerFactory.create(emptyIndexFile);
writer.write("[]");
writer.close();
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
你对此的测试:
class Test{
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock
private FileWriterFactory fileWriterFactory;
private Writer fileWriter = spy(new StringWriter());
File anyValidFile = new File(".");
@Test
public void initIndexFile_validFile_addsEmptyraces(){
//arrange
doReturn(fileWriter).when(fileWriterFactory).create(any(File.class));
// act
new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
//assert
verify(fileWriterFactory)create(anyValidFile);
assertEquals("text written to File", "[]", fileWriter.toString());
verify(fileWriter).close();
}
}
此外,您可以轻松检查您的 CuT 是否拦截了 IOException:
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void initIndexFile_missingFile_IndexFileInitializationException(){
//arrange
doReturnThrow(new IOException("UnitTest")).when(fileWriterFactory).create(any(File.class));
//assert
exception.expect(IndexFileInitializationException.class);
exception.expectMessage("Error initialization index file "+anyValidFile.getPath());
// act
new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
}
Nice! a factory just to test 3 lines of code! – Nicolas Filotto
这是一个很好的观点。
问题是:class 中是否有任何方法直接与 File
对象交互并且之后需要创建 FileWriter?
如果答案是 "no"(这很可能)遵循 KISS 原则,您应该直接注入一个 Writer
对象而不是工厂,并且您的方法没有 File 参数。
private final Writer writer;
public ClassUnderTest(@Inject Writer writer){
this.writer = writer;
}
@Override
public void initIndexFile() {
try {
writer.write("[]");
writer.close();
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
修改后的测试:
class Test{
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule public ExpectedException exception = ExpectedException.none();
@Mock
private FileWriterFactory fileWriterFactory;
@Mock
private Writer failingFileWriter;
private Writer validFileWriter = spy(new StringWriter());
File anyValidFile = new File(".");
@Test
public void initIndexFile_validFile_addsEmptyraces(){
//arrange
// act
new ClassUnderTest(validFileWriter).initIndexFile();
//assert
verify(fileWriterFactory)create(anyValidFile);
assertEquals("text written to File", "[]", fileWriter.toString());
verify(fileWriter).close();
}
@Test
public void initIndexFile_missingFile_IndexFileInitializationException(){
//arrange
doReturnThrow(new IOException("UnitTest")).when(failingFileWriter).write(anyString());
//assert
exception.expect(IndexFileInitializationException.class);
exception.expectMessage("Error initialization index file "+anyValidFile.getPath());
// act
new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
}
}
这是我在某些服务中的方法class。它是 public 所以它应该被测试。我根本不知道我应该测试什么。我会模拟 Writer
和 spyOn 函数调用,但使用此实现是不可能的(不是吗?)
我正在使用 Mockito
和 JUnit
现在,我只能创建函数来抛出并断言该异常
有什么帮助吗?
@Override
public void initIndexFile(File emptyIndexFile) {
try {
Writer writer = new FileWriter(emptyIndexFile);
writer.write("[]");
writer.close();
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
为了测试您的方法是否可以通过发送正确的命令与编写器正确交互,您的程序必须公开某种 "seam" 以便您的测试可以配置模拟 FileWriter
。我不熟悉 mockito
但一种方法是将 FileWriter
实例化封装在方法后面,然后您的测试可以将该方法重写为 return 模拟 FileWriter
.
假设File
是一个接口:
public Writer getFileWriter(File emptyIndexFile) {
return new FileWriter(emptyIndexFile);
}
这可以让您覆盖上述方法进行测试和 return 伪造 Writer
@Override
public Writer getFileWriter(File emptyIndexFile) {
return mockFileWriterInstance;
}
然后您的测试可以进行练习 initIndexFile
并对操作进行断言。使用模拟文件编写器应该很容易抛出 IOException
以便您可以练习错误处理逻辑。
您可以简单地在测试中为您的方法提供一个临时文件,然后简单地检查它是否包含预期的 []
,一旦超过就删除该文件。
类似于:
public class FileWritingTest {
// File to provide to the method initIndexFile
private File file;
/* This is executed before the test */
@Before
public void init() throws IOException {
// Create a temporary file
this.file = File.createTempFile("FileWritingTest", "tmp");
// Indicates that it should be removed on exit
file.deleteOnExit();
}
/* This is executed after the test */
@After
public void clean() throws IOException {
// Delete the file once test over
file.delete();
}
@Test
public void testInitIndexFile() throws IOException {
FileWriting fw = new FileWriting();
// Call the method
fw.initIndexFile(this.file);
// Check that the content is [] as expected
Assert.assertEquals("[]", new String(Files.readAllBytes(file.toPath())));
}
}
注意 1: 我依赖 new String(byte[])
这意味着我依赖默认字符编码,就像您在当前代码中所做的那样,但这不是一个好习惯, 我们应该明确设置字符编码以避免平台依赖。
注意2:假设你使用java7或更高版本,你应该考虑使用try-with-resources 声明正确关闭你的作者,你的代码将是:
public void initIndexFile(File emptyIndexFile) {
try (Writer writer = new FileWriter(emptyIndexFile)) {
writer.write("[]");
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
模拟依赖项是可能的,也是自然的,但是模拟在方法主体中声明的对象是不自然且棘手的。
我设想了 3 种解决方案:
1) 为什么不简单地断言文件是用预期的字符写入的,而不是模拟?
它避免了技巧,但如果您经常执行此任务并且想要对其进行单元测试,它可能会多余且缓慢。
2) 使局部变量成为一个实例字段来模拟它。这似乎真的不是一个干净的解决方案。如果您在执行此类处理的同一个 class 中有多个方法,则可能会重复使用同一个编写器或具有多个编写器字段。在这两种情况下,您都可能会产生副作用。
3) 如果你执行很多写操作并且你想真正隔离对编写器的调用,你有一个解决方案:重新设计你的代码以获得可测试的 class.
您可以提取依赖项来执行编写器处理。 class 可以提供一种带有执行指令所需参数的方法。我们可以称它为:WriteService
。
public class WriteService {
...
public void writeAndClose(Writer writer, String message){
try {
writer.write(message);
writer.close();
}
catch (IOException e) {
throw new IndexFileInitializationException("Error initialization index file " + emptyIndexFile.getPath());
}
}
}
这个 class 是可测试的,因为 writer 依赖是一个参数。
你这样调用新服务:
public class YourAppClass{
private WriteService writeService;
public YourAppClass(WriteService writeService){
this.writeService=writeService;
}
@Override
public void initIndexFile(File emptyIndexFile) {
Writer writer = new FileWriter(emptyIndexFile);
writeService.writeAndClose(writer,"[]");
}
}
现在 initIndexFile()
也可以通过模拟 WriteService
进行测试。
您可以检查 writeAndClose() 是在 writeService 上使用 good 参数调用的。
就我个人而言,我会使用第一种解决方案或第三种解决方案。
如果您认为添加特殊内容是业务逻辑,因此是您class的责任,然后创建 FileWriter 不是(根据 单一责任模式。
因此您应该使用在测试 下注入您的Class 的FileWriterFactory
。然后你可以模拟 FileWriterFactory
到 return Writer
接口的模拟实现,然后你可以检查它是否得到了预期的字符串。
你的 CuT 会变成这样:
private final WriterFactory writerFactory;
public ClassUnderTest(@Inject WriterFactory writerFactory){
this.writerFactory = writerFactory;
}
@Override
public void initIndexFile(File emptyIndexFile) {
try {
Writer writer = writerFactory.create(emptyIndexFile);
writer.write("[]");
writer.close();
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
你对此的测试:
class Test{
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock
private FileWriterFactory fileWriterFactory;
private Writer fileWriter = spy(new StringWriter());
File anyValidFile = new File(".");
@Test
public void initIndexFile_validFile_addsEmptyraces(){
//arrange
doReturn(fileWriter).when(fileWriterFactory).create(any(File.class));
// act
new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
//assert
verify(fileWriterFactory)create(anyValidFile);
assertEquals("text written to File", "[]", fileWriter.toString());
verify(fileWriter).close();
}
}
此外,您可以轻松检查您的 CuT 是否拦截了 IOException:
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void initIndexFile_missingFile_IndexFileInitializationException(){
//arrange
doReturnThrow(new IOException("UnitTest")).when(fileWriterFactory).create(any(File.class));
//assert
exception.expect(IndexFileInitializationException.class);
exception.expectMessage("Error initialization index file "+anyValidFile.getPath());
// act
new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
}
Nice! a factory just to test 3 lines of code! – Nicolas Filotto
这是一个很好的观点。
问题是:class 中是否有任何方法直接与 File
对象交互并且之后需要创建 FileWriter?
如果答案是 "no"(这很可能)遵循 KISS 原则,您应该直接注入一个 Writer
对象而不是工厂,并且您的方法没有 File 参数。
private final Writer writer;
public ClassUnderTest(@Inject Writer writer){
this.writer = writer;
}
@Override
public void initIndexFile() {
try {
writer.write("[]");
writer.close();
} catch (IOException e) {
throw new IndexFileInitializationException(
"Error initialization index file " + emptyIndexFile.getPath()
);
}
}
修改后的测试:
class Test{
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule public ExpectedException exception = ExpectedException.none();
@Mock
private FileWriterFactory fileWriterFactory;
@Mock
private Writer failingFileWriter;
private Writer validFileWriter = spy(new StringWriter());
File anyValidFile = new File(".");
@Test
public void initIndexFile_validFile_addsEmptyraces(){
//arrange
// act
new ClassUnderTest(validFileWriter).initIndexFile();
//assert
verify(fileWriterFactory)create(anyValidFile);
assertEquals("text written to File", "[]", fileWriter.toString());
verify(fileWriter).close();
}
@Test
public void initIndexFile_missingFile_IndexFileInitializationException(){
//arrange
doReturnThrow(new IOException("UnitTest")).when(failingFileWriter).write(anyString());
//assert
exception.expect(IndexFileInitializationException.class);
exception.expectMessage("Error initialization index file "+anyValidFile.getPath());
// act
new ClassUnderTest(fileWriterFactory).initIndexFile(anyValidFile);
}
}