数独sudoku(五)单元测试和正确性测试

  大家好,今天对之前实现的两个子功能进行单元测试和正确性测试。
  Github完整项目地址:https://github.com/surpasss/software-engineering

单元测试

  根据教材定义,单元是可以独立编译的最小组件,单元测试是根据详细设计规格的说明,重点测试分支、循环、以及独立路径的执行,发现更多单元内部错误。单元测试的内容集中在以下5个方面:模块接口、局部数据结构、执行路径、边界条件、异常处理。可以看出单元测试是白盒测试,由编码人员和相关技术人员完成。我对着那两份短短的代码和教材上的短短两面思考了很久,究竟如何对那两个子功能进行单元测试。从我之前的两次博客中的最终代码可以看出,代码结构挺清楚,也没用到什么复杂的数据结构,而且编译没有报错,运行结果也正确,所以我觉得代码功能上没有问题,因为确实比较短小,本质就一个函数,而被拆成了多个函数。
  我觉得对于我的代码而言,单元测试更多的是处理异常情况,其中可能会出现异常情况的地方就是文件,因为求解数独是需要传入残局文件的路径,如果该路径找不到应该报错(题目中已说明残局文件的数独格式正确,所以不用考虑)。但是呢,虽然残局文件的数独格式正确,但是毕竟是用户操作,可以输入任意字符串,所以也可能输入的路径存在,可以打开该文件,但是不是合格的残局,对此也应该处理。
  上一段考虑了两种可能异常情况,这两种异常情况我之前的代码都没有做处理。对于第一种情况,补充后的代码如下:

//打开文件
ifstream in (path);
if (!in)
{
	cout << "Open File Failed!" << endl;
	return;
}
ofstream out("sudoku.txt");
if (!out)
{
	cout << "Open File Failed!" << endl;
	return;
}

  对于第二种情况,也很好判断,补充后的代码如下:

for(int i = 0; i < 9; i++)
	for (int j = 0; j < 9; j++)
	{
		map[i][j] = s.at(index) - '0';
		if (map[i][j] < 0 || map[i][j] > 9)
		{
			cout << "File Format Error!" << endl;
			return;
		}
		index += 2;//跳过空格或者行末换行符
	}

  由于目前还尚未将两个子功能组合起来,因此搭建一个简单的驱动模块,驱动模块代码如下:

int main()
{
	char str[100] = "C:\\Users\\lenovo\\Desktop\\sudoku\\MySudoku\\Debug\\checkboard.txt";
	SolveSudoku(str);
	char str1[100] = "C:\\Users\\lenovo\\Desktop\\sudoku\\MySudoku\\Debug";
	SolveSudoku(str1);
	char str2[100] = "C:\\Users\\lenovo\\Desktop\\sudoku\\Others\\file.cpp";
	SolveSudoku(str2);
}

  运行截屏为:
  
  可以看到测试结果符合预期。

正确性测试

  具体情况还应该具体分析,在我的代码中,一个单元即一个模块,即一个子功能。我觉得子功能已经足够小了,不能放到集成测试中去,集成测试应该涉及到组装合并,所以接下来分别对两个子功能进行正确性测试,题目中已说明正确性测试的输入范围为1-1000。

测试生成终局

  虽然在软件设计中我已经分析了生成终局方法的可行性,但是仍然需要进行测试,因为可能存在方法以外的问题。对于生成的终局文件,需要测是否是个合法的数独、每个数独是否唯一。首先生成1000个终局,保存在sudoku.txt中,然后判断每个数独是否是合法的数独,即每列、每行、每个3×3方格都包含19。方法为:一次性读取文件中的所有数据保存在长字符串中,然后每次截取一个数独保存在整型二维数组中,用IsSudoku函数判断数独的每一行、每一列、每个3×3方格是否包含19。判断的方法很简单,可以用一个数组judge[10]保存涵盖情况,judge[i] == 0表示数字i不存在,judge[i] == 1表示数字i存在。judge数组每次初始化为0后,遍历该行或者该列或者该3×3方格的9个小方格,把小方格中数字对应的judge[i]置为1。遍历完9个小方格后,判断judge[i](1≦i≦9)是否都为1,如果不是,则说明不是合法数独,直接退出IsSudoku函数,并打印该数独的序号,遍历完所有行、列、方块后返回true。
  接下来测试每个数独是否唯一,用string strings[1000]数组保存每个数独的所有字符,然后调用IsUnique函数比较两两字符串是否相等,如果相等,打印这两个字符串的序号,如果都不相等返回true。
  测试生成终局正确性的完整代码如下:

#include "define.h"

static int map[9][9];
int judge[10];
string strings[1000];

bool IsSudoku()
{
	for (int i = 0; i < 9; i++)//遍历1-9行
	{
		memset(judge, 0, sizeof(judge));
		for (int j = 0; j < 9; j++)//遍历该行的每个小方格
		{
			int x = map[i][j];
			judge[x] = 1;
		}
		for (int k = 1; k <= 9; k++)
		{
			if (judge[k] != 1)
				return false;
		}
	}
	for (int i = 0; i < 9; i++)//遍历1-9列
	{
		memset(judge, 0, sizeof(judge));
		for (int j = 0; j < 9; j++)//遍历该列的每个小方格
		{
			int x = map[j][i];
			judge[x] = 1;
		}
		for (int k = 1; k <= 9; k++)
		{
			if (judge[k] != 1)
				return false;
		}
	}
	//遍历每个3*3方块
	for(int i = 0; i < 9; i += 3)
		for (int j = 0; j < 9; j += 3)
		{
			memset(judge, 0, sizeof(judge));
			//遍历该方块的每个小方格
			for (int a = 0; a < 3; a++)
				for (int b = 0; b < 3; b++)
				{
					int x = map[i + a][j + b];
					judge[x] = 1;
				}
			for (int k = 1; k <= 9; k++)
			{
				if (judge[k] != 1)
					return false;
			}
		}
	return true;
}

bool IsUnique(int n)
{
	bool flag = true;
	for(int i = 0; i < n; i++)
		for (int j = i + 1; j < n; j++)
		{
			if (strings[i] == strings[j])
			{
				printf("Wrong! position %d and position %d are same.\n", i, j);
				flag = false;
			}
		}
	return flag;
}


void TestCreate()
{
	//一次性读文件,保存在字符串s中
	ifstream in("1000.txt");
	if (!in)
	{
		cout << "Open File Failed!" << endl;
		return;
	}
	stringstream buf;
	buf << in.rdbuf();//一次性读取
	string s = buf.str();
	in.clear();
	in.close();

	//判断每个数独是否合法
	int index = 0;//字符串s的当前下标
	bool flag = true;
	while (index + 161 <= s.length())
	{
		for (int i = 0; i < 9; i++)
			for (int j = 0; j < 9; j++)
			{
				map[i][j] = s.at(index) - '0';
				index += 2;//跳过空格和行末换行符
			}
		index += 1;//跳过数独间的换行符
		if (!IsSudoku())
		{
			cout << "Wrong! " << "Postion: " << (index - 1) / 163 << endl;
			flag = false;
		}
	}
	if (flag == true)
	{
		cout << "Right, all sudokus are legal." << endl;
	}

	//判断每个数独是否唯一
	int num = (s.length() + 2) / 163;
	for (int i = 0; i < num; i++)
	{
		strings[i] = s.substr(i * 163, 161);
	}
	if (IsUnique(num))
	{
		cout << "Right, all sudokus are unique." << endl;
	}
}

  已保证测试代码无误的情况下,运行结果为:
  

测试求解数独

  之前在求解数独的博客中,分别求解1e6个全是0的残局和1e6个不需填充的终局,测试结果无误。测试只能发现错误,并不能证明完全正确,所以还应当考虑更多的方法。这次我采取的方法是:对之前生成的保存1000个终局的1000.txt文件的每一个数独,每一行随机选择一个数字置换为0,生成残局文件test_solve.txt。然后对该残局文件进行求解数独,把求解结果保存在sudoku.txt文件中。之后一次性读取1000.txt和sudoku.txt文件,比较这两个文件的内容是否相等。如果相等,则说明求解无误,否则说明存在问题。
  这个方法理论上没有问题,因为生成的每个残局只有一种求解结果,就是1000.txt文件中的对应终局。直接给出保证无误的测试代码,如下:

#include "define.h"

void CreateTestFile()
{
	//一次性读取完整终局文件,保存在字符串s中
	ifstream in("1000.txt");
	if (!in)
	{
		cout << "Open 1000.txt Failed!" << endl;
		return;
	}
	stringstream buf;
	buf << in.rdbuf();//一次性读取
	string s = buf.str();
	in.clear();
	in.close();

	//随机选择每一行的某个数字置换为0
	int index = 0;//字符串s的当前下标
	srand((unsigned)time(NULL));
	while (index + 161 <= s.length())
	{
		for (int i = 0; i < 9; i++)
		{
			int a = rand() % 9;//生成0-8以内的随机数
			//cout << a << " ";//打印随机数a的值
			s.at(index + a * 2) = '0';
			index += 18;//跳至下一行初始位置
		}
		//cout << endl;
		index += 1;//跳过数独间的换行符
	}

	//把修改后的字符串s写入test_solve.txt文件
	ofstream out("test_solve.txt");
	if (!out)
	{
		cout << "Open test_solve.txt Failed!" << endl;
		return;
	}
	out.write(s.c_str(), index - 2);
	out.clear();
	out.close();
	cout << "Create test_solve.txt Successed." << endl;
}

void TestSolve()
{
	CreateTestFile();//生成残局文件test_solve.txt
	char fileName[] = "test_solve.txt";
	SolveSudoku(fileName);//求解残局文件,结果保存在sudoku.txt文件中

	//一次性读取1000.txt
	ifstream in_1("1000.txt");
	stringstream buf_1;
	buf_1 << in_1.rdbuf();//一次性读取
	string s1 = buf_1.str();
	in_1.clear();
	in_1.close();

	//一次性读取sudoku.txt
	ifstream in_2("sudoku.txt");
	stringstream buf_2;
	buf_2 << in_2.rdbuf();
	string s2 = buf_2.str();
	in_2.clear();
	in_2.close();

	//比较1000.txt和sudoku.txt的内容是否相等
	if (s1 == s2)
	{
		cout << "Confirm Right." << endl;
	}
	else
	{
		cout << "Confirm Wrong!" << endl;
	}
}

  运行结果为:
  

posted @ 2020-01-07 20:40  追梦人啊  阅读(679)  评论(0)    收藏  举报