这是我在某个服务类中的方法。它是公开的,所以应该进行测试。我根本不知道该测试什么。我会模拟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()
);
}
}
如果您觉得添加特殊内容是业务逻辑,因此是类的责任的话,那么创建FileWriter就不是(根据单一责任模式)。
因此,您应该使用一个FileWriterFactory
,它被注入到测试中的类中。然后,您可以对FileWriterFactory
进行模拟,以返回Writer
接口的模拟实现,在该接口上,您可以检查它是否获得了预期的String。
你的提示将更改为:
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);
}
不错!一个只测试3行代码的工厂Nicolas Filotto
这是一个很好的观点。
问题是:该类中是否有任何方法直接与File
对象交互,并需要在之后创建FileWriter?
如果答案是"否"(很可能是这样),遵循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);
}
}
要测试您的方法是否可以通过发送正确的命令与编写器正确交互,您的pogram必须公开某种"seam",以便您的测试可以配置模拟FileWriter
。我不熟悉mockito
,但一种方法是将FileWriter
实例化封装在一个方法后面,然后您的测试可以覆盖该方法以返回模拟FileWriter
。
假设File
是一个接口:
public Writer getFileWriter(File emptyIndexFile) {
return new FileWriter(emptyIndexFile);
}
这可以让你覆盖上面的测试方法,并返回一个假的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())));
}
}
NB 1:我依赖new String(byte[])
,这意味着我像您在当前代码中那样依赖默认的字符编码,但这不是一个好的做法,我们应该显式设置字符编码以避免依赖平台。
NB 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) 将局部变量作为一个实例字段来模拟它。这似乎真的不是一个干净的解决方案。如果同一类中有多个方法进行这种处理,则可能会重用同一个编写器或具有多个编写器字段。在这两种情况下,你都可能有副作用。
3) 如果您执行了许多写操作,并且希望真正隔离对编写器的调用,那么您就有了一个解决方案:重新设计代码,使其具有可测试的类。
您可以提取一个依赖项来执行写入程序处理。该类可以为方法提供执行指令所需的参数。我们可以称之为: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());
}
}
}
此类是可测试的,因为编写器依赖项是一个参数。
你这样称呼新服务:
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
进行测试。您可以使用正确的参数检查是否在writeService上调用了tat writeAndClose()。
就我个人而言,我会使用第一种解决方案或第三种解决方案