【问题标题】:Suggestions on how to test a BufferedReader and FileReader that takes in strings and puts them into an ArrayList关于如何测试接收字符串并将它们放入 ArrayList 的 BufferedReader 和 FileReader 的建议
【发布时间】:2016-10-27 18:47:18
【问题描述】:

我有一个类,它有一个方法可以逐行读取文本文件,然后将每一行放入字符串的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 测试此方法的一些建议。我听说涉及文本文件的读者可能很难测试,并且创建一个临时文件不是最佳实践,因为它会占用内存?任何建议将不胜感激。

【问题讨论】:

  • 我会创建一个包含两三行文本的临时文件。我不明白为什么这不是“最佳实践”或“用完内存”。当然,如果类允许从外部设置数据源(即,给定的Reader 对象),则不需要实际的文件。模拟 Java IO API 当然不是一个好习惯。
  • 提醒一下:如果您正在练习 TDD,请先编写测试,这样您就不会在查看代码时想知道如何测试它。

标签: java file-io mocking tdd mockito


【解决方案1】:

由于文件名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 会更有意义。

【讨论】:

  • 阅读器是这个类的一个依赖。 OOP 的基本规则之一是单一职责。从这个意义上说,为依赖项创建对象不是这个类的职责。事实上,创建对象的唯一类(除了 DTO 或像 BufferedReader 这样的包装器)应该是最初构造程序对象树的工厂或类似的东西。坚持这种方法几乎总是会为您提供良好的可测试类,但工厂和初始化程序类除外。
【解决方案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% 的线路覆盖率和纯单元测试。

【讨论】:

    【解决方案3】:

    提取依赖项,以便在测试时可以模拟/存根和注入它们。它还有助于将类的范围缩小到其核心职责。

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

    【讨论】:

      【解决方案4】:

      好吧,您似乎在尝试测试框架,在这种特殊情况下是 JDK。我会考虑更方便的 API:

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-08-16
        • 2018-04-25
        • 2020-03-15
        • 2011-05-20
        • 1970-01-01
        • 2017-01-25
        • 2013-05-31
        • 2012-10-09
        相关资源
        最近更新 更多