我有一个类,它有一个方法,逐行读取文本文件,然后将每一行放入字符串的ArrayList
。下面是我的代码:
public class ReadFile {
public List<String> showListOfCourses() throws IOException {
String filename = "countriesInEurope.txt";
FileReader fr = new FileReader(filename);
BufferedReader br = new BufferedReader(fr);
List<String> courseList = new ArrayList<>();
while (true) {
String line = br.readLine();
if (line == null) {
break;
}
courseList.add(line);
}
br.close();
return courseList;
}
}
我希望有一些关于如何通过Mockito
测试这个方法的建议,包括安排/行为/断言。我听说读者涉及文本文件可能是棘手的测试,并创建一个临时文件是不是最佳实践,因为它占用内存?如有任何建议,我将不胜感激。
由于文件名countriesInEurope.txt
是硬编码在您的实现,这是不可测试的。使其可测试的一个好方法是重构该方法,以Reader
作为参数:
public List<String> showListOfCourses(Reader reader) throws IOException {
BufferedReader br = new BufferedReader(reader);
List<String> courseList = new ArrayList<>();
// ...
return courseList;
}
你的主实现可以传递一个FileReader
给this。另一方面,在测试时,您的测试方法可以传递一个StringReader
实例,这很容易创建一个示例内容作为一个简单字符串,不需要临时文件,例如:
@Test
public void showListOfCourses_should_read_apple_orange_banana() {
Reader reader = new StringReader("applenorangenbanana");
assertEquals(Arrays.asList("apple", "orange", "banana"), showListOfCourses(reader));
}
顺便说一句,这个方法的名字不好,因为它没有"显示"任何东西。readListOfCourses
更有意义
有问题的测试行是
String filename = "countriesInEurope.txt";
FileReader fr = new FileReader(filename);
因为- 文件名是硬编码的,不能为测试 替换
-
FileReader
使用底层系统io,这是很难模拟的
尽管如此,还是有一些方法可以使你的代码可测试
1。引入一个构造函数来参数化ReadFile
对象创建
public class ReadFile {
private String filename;
public ReadFile(String filename) {
this.filename = filename;
}
public List<String> showListOfCourses() throws IOException {
FileReader fr = new FileReader(filename);
...
return courseList;
}
}
在您的测试中,您可以创建一个ReadFile
对象,它使用一些测试文件。使用这种策略,您可以实现100%的行覆盖率,但是您的测试必须访问文件系统上的真实文件。所以你不能把它写成一个纯粹的单元测试。
2。将有问题的行提取到一个可重写的方法
public class ReadFile {
public List<String> showListOfCourses() throws IOException {
Reader courcesReader = openCoursesFile();
BufferedReader br = new BufferedReader(courcesReader);
List<String> courseList = new ArrayList<>();
// ...
return courseList;
}
protected Reader openCoursesFile() throws FileNotFoundException {
return new FileReader("countriesInEurope.txt");
}
}
在您的测试中,您可以子类化ReadFile
类并覆盖Reader openCoursesFile()
方法。例如
@Test
public void showCources() throws IOException {
ReadFile readFile = new ReadFile() {
protected Reader openCoursesFile() throws java.io.FileNotFoundException {
return new StringReader("GermanynItalynFrance");
};
};
List<String> showListOfCourses = readFile.showListOfCourses();
Assert.assertEquals(Arrays.asList("Germany", "Italy", "France"), showListOfCourses);
}
使用此策略,您可以将测试编写为纯单元测试,因为您用StringReader
(仅在内存中)替换了文件访问。唯一不能测试的行是
return new FileReader("countriesInEurope.txt");
所以没有100%的行覆盖率
编辑
3。引入构造函数并向其传递Reader
对象创建
public class ShowListOfCoursesReader {
private Reader reader;
public ReadFile(Reader reader) {
this.reader = reader;
}
public List<String> read() throws IOException {
// read with reader and transform each line to the
// output object.
// In your case just the line you read, but it could
// also be a date or a address object
...
return courseList;
}
}
在您的测试中,您可以创建一个ShowListOfCoursesReader
对象,该对象使用通过的读取器。阅读器也可以是StringReader
。使用此策略,您可以实现100%的行覆盖率和纯单元测试。
提取依赖项,以便在测试时可以模拟/存根并注入它们。它还有助于将类的作用域缩小到它的核心职责。
public class CourseReader {
private BufferedReader reader;
public CourseReader(BufferedReader br) {
this.reader = br;
}
public List<String> GetListOfCourses() throws IOException {
List<String> courseList = new ArrayList<>();
String line;
while((line = reader.readLine()) != null) {
courseList.add(line);
}
return courseList;
}
}
现在要测试这个类,可以预先安排依赖项。
@Test
public void GetListOfCourses_should_read_3_Courses() {
//Arrange
List<String> expected = Arrays.asList("course1", "course2", "course3");
Reader reader = new StringReader("course1ncourse2ncourse3");
BufferedReader bufferedReader = new BufferedReader(reader);
CourseReader sut = new CourseReader(bufferedReader);
//Act
List<String> actual = sut.GetListOfCourses();
//Assert
assertEquals(expected, actual);
}
可以进一步重构以抽象出实现细节。
public interface IReaderWrapper {
String readLine();
void close();
}
并使用它作为依赖项
public class CourseReader {
private IReaderWrapper reader;
public CourseReader(IReaderWrapper reader) {
this.reader = reader;
}
public List<String> GetListOfCourses() throws IOException {
List<String> courseList = new ArrayList<>();
String line;
while((line = reader.readLine()) != null) {
courseList.add(line);
}
reader.close();
return courseList;
}
}
这样在测试时只需要模拟接口。接口的实现将考虑如何实际读取数据。
@Test
public void GetListOfCourses_should_read_3_Courses() {
//Arrange
List<String> expected = Arrays.asList("course1", "course2", "course3");
IReaderWrapper mockedReader = mock(IReaderWrapper.class);
when(mockedReader.readLine())
.thenReturn(expected[0], expected[1], expected[2], null);
CourseReader sut = new CourseReader(mockedReader);
//Act
List<String> actual = sut.GetListOfCourses();
//Assert
assertEquals(expected, actual);
//verify that the close method was called.
verify(mockedReader).close();
}
嗯,看起来你是在测试框架,在这个特殊的例子中是JDK。我会考虑更方便的API:
Files.readAllLines(Paths.get("blablabla.txt"));
或
Files.lines(Paths.get("blablabla.txt"));
和cover由测试的更高抽象层-使用字符串列表的地方。