前言
总所周知测试是写程序必不可少的一项。对于IDEA和java来说,junit测试是一个比较好的选择。
安装junit
首先在IDEA上安装junit的插件;
前往官方github下载junit;
在项目结构中添加junit依赖。
具体可以参考:
使用junit
junit最基础的使用,IDEA中使用JUnit4单元测试已经说的十分明白了,而且举的例子也非常出色,我在这里就不加赘述了。
大概来说,就是新建一个测试类,然后搞明白@Before
,@After
,@Test
,assertEquals
的基本意思就行了。
模拟输入和捕获输出
上面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的
}
}
文件读入以及多组数据
有的时候,输入输出会特别的长,或者输入输出有很多组,使用上面这个死板的方法就会显得非常臃肿。所以我们需要进行文件读入。
文件读入
使用BufferedReader
和FileReader
可以之间读取一个文件中的全部字符,并将其转化为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;
}
}