如何测试将字符串读取并放入ArrayList的BufferedReader和FileReader建议

3

我有一个类,其中有一个方法逐行读取文本文件,然后将每一行放入一个 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进行安排/执行/断言测试的建议。我听说涉及文本文件的读取可能很棘手,而且创建临时文件并不是最佳实践,因为它会占用内存。非常感谢您的任何建议。


我会创建一个包含两三行文本的临时文件。我不明白为什么这不是“最佳实践”或“占用内存”。当然,如果该类允许从外部设置数据源(即给定的Reader对象),则不需要实际文件。模拟Java IO API肯定不是一个好的做法。 - Rogério
提醒一下:如果你正在练习TDD,那么你应该先编写测试,这样你就不会看着代码疑惑如何进行测试。 - Mike Stockdale
4个回答

8

由于文件名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));
}

顺便提一下,这个方法的名称不太好, 因为它没有“展示”任何内容。 readListOfCourses 更有意义。


1
读者是这个类的依赖项。面向对象编程的基本规则之一是单一职责原则。在这个意义上,为依赖项创建对象不是这个类的职责。事实上,除了DTO或像BufferedReader这样的包装器之类的类之外,唯一创建对象的类应该是工厂或类似的类,它们最初构建程序的对象树。坚持这种方法几乎总是会给你带来良好的可测试类,除了工厂和初始化器类。 - Timothy Truckle

2

测试中有问题的行为:

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");
    }

}

在你的测试中,你可以继承ReadFile类并重写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;
    }

}

在您的测试中,您可以创建一个使用传递的读取器的 ShowListOfCoursesReader 对象。读取器也可以是 StringReader。 采用这种策略,您可以实现100%的代码行覆盖率和纯单元测试。


1
提取依赖项,以便在测试时可以进行模拟/存根并注入。 这也有助于将类的范围缩小到其核心职责。
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("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();
}

1

嗯,看起来你想测试框架,特别是JDK。我会考虑使用更方便的API:

Files.readAllLines(Paths.get("blablabla.txt"));

或者

Files.lines(Paths.get("blablabla.txt"));

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接