关于如何测试接收字符串并将其放入 ArrayList 的 BufferedReader 和 FileReader 的建议
Suggestions on how to test a BufferedReader and FileReader that takes in strings and puts them into an ArrayList
我有一个 class,它有一个方法可以逐行读取文本文件,然后将每一行放入一个 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;
}
}
我希望就如何通过涉及 Arrange/Act/Assert 的 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
传递给它。另一方面,在测试时,您的测试方法可以传递一个 StringReader
实例,它很容易用简单字符串的示例内容创建,不需要临时文件,例如:
@Test
public void showListOfCourses_should_read_apple_orange_banana() {
Reader reader = new StringReader("apple\norange\nbanana");
assertEquals(Arrays.asList("apple", "orange", "banana"), showListOfCourses(reader));
}
顺便说一句,方法的名字不好,
因为它没有 "show" 任何东西。
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");
}
}
在您的测试中,您可以接着 class ReadFile
class 并覆盖 Reader openCoursesFile()
方法。例如
@Test
public void showCources() throws IOException {
ReadFile readFile = new ReadFile() {
protected Reader openCoursesFile() throws java.io.FileNotFoundException {
return new StringReader("Germany\nItaly\nFrance");
};
};
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;
}
}
然后在您的测试中,您可以创建一个使用传递的 reader 的 ShowListOfCoursesReader
对象。 reader 也可以是 StringReader
。
使用此策略,您可以实现 100% 的行覆盖率和纯单元测试。
提取依赖项以便它们可以 mocked/stubbed 并在测试时注入。它还有助于将 class 的范围缩小到其核心职责。
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;
}
}
现在要测试这个 class 可以事先安排依赖项。
@Test
public void GetListOfCourses_should_read_3_Courses() {
//Arrange
List<String> expected = Arrays.asList("course1", "course2", "course3");
Reader reader = new StringReader("course1\ncourse2\ncourse3");
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"));
并通过测试更高层的抽象覆盖 - 一个使用字符串列表的地方。
我有一个 class,它有一个方法可以逐行读取文本文件,然后将每一行放入一个 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;
}
}
我希望就如何通过涉及 Arrange/Act/Assert 的 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
传递给它。另一方面,在测试时,您的测试方法可以传递一个 StringReader
实例,它很容易用简单字符串的示例内容创建,不需要临时文件,例如:
@Test
public void showListOfCourses_should_read_apple_orange_banana() {
Reader reader = new StringReader("apple\norange\nbanana");
assertEquals(Arrays.asList("apple", "orange", "banana"), showListOfCourses(reader));
}
顺便说一句,方法的名字不好,
因为它没有 "show" 任何东西。
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");
}
}
在您的测试中,您可以接着 class ReadFile
class 并覆盖 Reader openCoursesFile()
方法。例如
@Test
public void showCources() throws IOException {
ReadFile readFile = new ReadFile() {
protected Reader openCoursesFile() throws java.io.FileNotFoundException {
return new StringReader("Germany\nItaly\nFrance");
};
};
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;
}
}
然后在您的测试中,您可以创建一个使用传递的 reader 的 ShowListOfCoursesReader
对象。 reader 也可以是 StringReader
。
使用此策略,您可以实现 100% 的行覆盖率和纯单元测试。
提取依赖项以便它们可以 mocked/stubbed 并在测试时注入。它还有助于将 class 的范围缩小到其核心职责。
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;
}
}
现在要测试这个 class 可以事先安排依赖项。
@Test
public void GetListOfCourses_should_read_3_Courses() {
//Arrange
List<String> expected = Arrays.asList("course1", "course2", "course3");
Reader reader = new StringReader("course1\ncourse2\ncourse3");
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"));
并通过测试更高层的抽象覆盖 - 一个使用字符串列表的地方。