关于如何测试接收字符串并将其放入 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);

因为

  1. 文件名是硬编码的,不能为测试替换
  2. 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"));

并通过测试更高层的抽象覆盖 - 一个使用字符串列表的地方。