使用junit4对java程序进行测试


前言

总所周知测试是写程序必不可少的一项。对于IDEA和java来说,junit测试是一个比较好的选择。

安装junit

首先在IDEA上安装junit的插件;

前往官方github下载junit;

在项目结构中添加junit依赖。

具体可以参考:

IDEA中添加junit4的三种方法(详细步骤操作)

IDEA中使用JUnit4单元测试

使用junit

junit最基础的使用,IDEA中使用JUnit4单元测试已经说的十分明白了,而且举的例子也非常出色,我在这里就不加赘述了。

大概来说,就是新建一个测试类,然后搞明白@Before@After@TestassertEquals的基本意思就行了。

模拟输入和捕获输出

上面junit的最基本的用法是测试方法或者类的行为是否正常,但是我们希望junit能够帮助我们测试样例数据,而我们的main方法的交互方式是输入和输出,而不是传参和返回,所以不能直接使用上面的方式。

因此我们通过输入输出重定向的方式来模拟输入和捕获输出。可以写以下代码:

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;

import static org.junit.Assert.assertEquals;

public class SampleTest
{
    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    @Before
    public void setUpOutput()
    {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data)
    {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput()
    {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput()
    {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    @Test
    public void sample1()
    {
        final String testStringIn = """
                2021/7/1-Jack@JayChou :"Hello!";2021/7/3-JayChou@buaaer :"Hahaha";
                2021/7/5-JayChou@Mike :"emmmm";         2021/7/8-JayChou@buaaer :"Hahaha";
                2021/7/8-JayChou:"Hahaha"; 2021/5/3-Mike:"he@buaaer is unhappy";
                END_OF_MESSAGE
                qdate 2021/7/1
                qsend "JayChou"
                qrecv "buaaer"
                """; // 多行字符串,这里填样例输入
        final String testStringOut = """
                2021/7/1-Jack@JayChou :"Hello!";
                2021/7/3-JayChou@buaaer :"Hahaha";
                2021/7/5-JayChou@Mike :"emmmm";
                2021/7/8-JayChou@buaaer :"Hahaha";
                2021/7/8-JayChou:"Hahaha";
                2021/7/3-JayChou@buaaer :"Hahaha";
                2021/7/8-JayChou@buaaer :"Hahaha";
                2021/5/3-Mike:"he@buaaer is unhappy";
                """; // 多行字符串,这里填样例输出

        provideInput(testStringIn);
        MainClass.main(new String[0]);
        assertEquals(testStringOut, getOutput().replace("\r\n", "\n"));
        // 这里根据实际情况replace。一般来说是需要replace的
    }
}

文件读入以及多组数据

有的时候,输入输出会特别的长,或者输入输出有很多组,使用上面这个死板的方法就会显得非常臃肿。所以我们需要进行文件读入。

文件读入

使用BufferedReaderFileReader可以之间读取一个文件中的全部字符,并将其转化为String,具体代码如下:

private String readFromFile(String fileName) throws IOException
{
    BufferedReader reader = new BufferedReader(new FileReader(fileName));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
        stringBuilder.append('\n');
    }

    return stringBuilder.toString();
}

多组数据

junit提供了一种运行多组测试的方式:Parameterized

具体来说,就是当你使用@RunWith (Parameterized.class)修饰class时,junit会首先调用被@Parameterized.Parameters修饰的static方法,这个方法必须返回一个Collection。然后junit会遍历这个Collection,把遍历到的值传入class的构造方法中,生成一个测试类的对象。然后再运行@Test方法。

完整代码

这里我写了一种文件读入的方法,大家可以直接使用:

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.List;
import java.util.HashMap;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileReader;
import java.io.File;

import static org.junit.Assert.assertEquals;

@RunWith (Parameterized.class)
public class PublicTest // 类名根据需要重构
{
    private static final String PATH = "./test/public_test"; // 测试数据目录
    private static final String SUFFIX_IN = ".in"; // 测试点输入文件后缀
    private static final String SUFFIX_OUT = ".out"; // 测试点输出文件后缀

    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    private final String inputFileName;
    private final String expectedFileName;

    public PublicTest(String input, String expected)
    {
        this.inputFileName = input;
        this.expectedFileName = expected;
    }

    @Before
    public void setUpOutput()
    {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data)
    {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput()
    {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput()
    {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    private String readFromFile(String fileName) throws IOException
    {
        BufferedReader reader = new BufferedReader(new FileReader(fileName));
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
            stringBuilder.append('\n');
        }

        return stringBuilder.toString();
    }

    @Test (timeout = 10000)
    public void runTest() throws IOException
    {
        final String testStringIn = readFromFile(inputFileName);
        final String testStringOut = readFromFile(expectedFileName);

        provideInput(testStringIn);
        MainClass.main(new String[0]);
        assertEquals(testStringOut, getOutput().replace("\r\n", "\n"));
    }

    @Parameterized.Parameters
    public static List<String[]> getParams()
    {
        File file = new File(PATH);
        File[] fs = file.listFiles();
        HashMap<String, File> fInMap = new HashMap<>();
        HashMap<String, File> fOutMap = new HashMap<>();
        assert fs != null;
        for (File f : fs) {
            String name = f.getName();
            int index = name.lastIndexOf(".");
            String prefix;
            String suffix;
            if (index == -1) {
                System.err.print("Warning: file ");
                System.err.print("\"" + PATH + "/" + f.getName() + "\"");
                System.err.println(" does NOT appear to be a test data.");
                continue;
            }
            prefix = name.substring(0, index);
            suffix = name.substring(index);
            if (suffix.equals(SUFFIX_IN)) {
                fInMap.put(prefix, f);
            } else if (suffix.equals(SUFFIX_OUT)) {
                fOutMap.put(prefix, f);
            } else {
                System.err.print("Warning: file ");
                System.err.print("\"" + PATH + "/" + f.getName() + "\"");
                System.err.println(" does NOT appear to be a test data.");
            }
        }
        ArrayList<String[]> testData = new ArrayList<>();
        for (String name : fInMap.keySet()) {
            File in = fInMap.get(name);
            File out = fOutMap.get(name);
            if (out == null) {
                System.err.print("Warning: file ");
                System.err.print("\"" + PATH + "/" + name + SUFFIX_IN + "\"");
                System.err.println(" does NOT appear to have a corresponding out file.");
                continue;
            }
            String[] pair = { PATH + "/" + in.getName(), PATH + "/" + out.getName() };
            testData.add(pair);
        }
        return testData;
    }
}

测试结果

测试结果如下:

细节

注意测试是不能添加到平台上进行评测的(会CE),所以我们必须要采用git多分支的方式进行。

通常的做法是,我们首先master分支中创建README,然后commit,之后进行git checkout -b develop。一切改动都在develop分支进行。测试无误后,执行git checkout master以及git checkout develop src/*即可提交并推送到远程仓库。

后记

从课程网站上下载数据非常麻烦,所以如果你有一个自动下载脚本的话……


2022年10月2日 更新

如果你运行测试出错了却找不到哪个文件出错了的话,可以在80行左右的位置,runTest方法里面,加入

System.err.println("File In: " + inputFileName);
System.err.println("File Ans: " + expectedFileName);

这样就可以看见读入的文件是哪一个了,如下图所示:


2022年10月4日 更新

之前的测试类只能测试一个文件夹下的数据,不能测试一个文件夹下的子文件夹下的数据,因此做出改进。

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.List;
import java.util.HashMap;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.FileReader;
import java.io.File;

import static org.junit.Assert.assertEquals;

@RunWith (Parameterized.class)
public class InfiniteTest
{
    private static final String PATH = "./test";
    private static final String SUFFIX_IN = ".in";
    private static final String SUFFIX_OUT = ".out";

    private final InputStream systemIn = System.in;
    private final PrintStream systemOut = System.out;

    private ByteArrayInputStream testIn;
    private ByteArrayOutputStream testOut;

    private final String inputFileName;
    private final String expectedFileName;

    public InfiniteTest(String input, String expected)
    {
        this.inputFileName = input;
        this.expectedFileName = expected;
    }

    @Before
    public void setUpOutput()
    {
        testOut = new ByteArrayOutputStream();
        System.setOut(new PrintStream(testOut));
    }

    private void provideInput(String data)
    {
        testIn = new ByteArrayInputStream(data.getBytes());
        System.setIn(testIn);
    }

    private String getOutput()
    {
        return testOut.toString();
    }

    @After
    public void restoreSystemInputOutput()
    {
        System.setIn(systemIn);
        System.setOut(systemOut);
    }

    private String readFromFile(String fileName) throws IOException
    {
        BufferedReader reader = new BufferedReader(new FileReader(fileName));
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
            stringBuilder.append('\n');
        }

        return stringBuilder.toString();
    }

    @Test (timeout = 10000)
    public void runTest() throws IOException
    {
        System.err.println("The input file is " + "\"" + inputFileName + "\"");
        System.err.println("The answer file is " + "\"" + expectedFileName + "\"");
        final String testStringIn = readFromFile(inputFileName);
        final String testStringOut = readFromFile(expectedFileName);

        provideInput(testStringIn);
        MainClass.main(new String[0]);
        assertEquals(testStringOut, getOutput().replace("\r\n", "\n"));
    }

    private static final HashMap<String, File> fInMap = new HashMap<>();
    private static final HashMap<String, File> fOutMap = new HashMap<>();

    private static void getFiles(File directory)
    {
        assert directory != null;
        File[] files = directory.listFiles();
        assert files != null;
        for (File f : files) {
            if (f.isDirectory()) {
                getFiles(f);
                continue;
            }
            String name = f.getPath();
            int index = name.lastIndexOf(".");
            String prefix;
            String suffix;
            if (index == -1) {
                System.err.print("Warning: file ");
                System.err.print("\"" + f.getPath() + "\"");
                System.err.println(" does NOT appear to be a test data.");
                continue;
            }
            prefix = name.substring(0, index);
            suffix = name.substring(index);
            if (suffix.equals(SUFFIX_IN)) {
                fInMap.put(prefix, f);
            } else if (suffix.equals(SUFFIX_OUT)) {
                fOutMap.put(prefix, f);
            } else {
                System.err.print("Warning: file ");
                System.err.print("\"" + f.getPath() + "\"");
                System.err.println(" does NOT appear to be a test data.");
            }
        }
    }

    @Parameterized.Parameters
    public static List<String[]> getParams()
    {
        getFiles(new File(PATH));
        ArrayList<String[]> testData = new ArrayList<>();
        for (String name : fInMap.keySet()) {
            File in = fInMap.get(name);
            File out = fOutMap.get(name);
            if (out == null) {
                System.err.print("Warning: file ");
                System.err.print("\"" + in.getPath() + "\"");
                System.err.println(" does NOT appear to have a corresponding out file.");
                continue;
            }
            String[] pair = { in.getPath(), out.getPath() };
            testData.add(pair);
        }
        System.err.println();
        return testData;
    }
}

评论
  目录