你想要例子吗?上学期我修了一门编译器课程。在这门课程中,我们必须编写一个寄存器分配器。简单来说,我的程序可以总结如下:
输入:一个以ILOC格式编写的文件,这是一种为我的教科书而创造的伪汇编语言。文件中的指令具有像“r<number>
”这样的寄存器名称。问题在于程序使用尽可能多的寄存器,通常大于目标机器上的寄存器数量。
输出:另一个以ILOC格式编写的文件。这次,指令被重新编写,以使用允许的正确最大寄存器数。
为了编写这个程序,我必须创建一个能够解析ILOC文件的类。我为那个类编写了一堆测试。以下是我的测试(实际上我有更多,但为了缩短长度而删除了它们。我还添加了一些注释来帮助你阅读)。我用C++编写了这个项目,所以我使用了Google的C++测试框架(googletest),位于这里。
在展示代码之前...让我说一些关于基本结构的事情。基本上,有一个测试类。你可以把一堆通用设置放在测试类中。然后有一些名为TEST_F的测试宏。测试框架会捕捉到这些宏并理解它们需要作为测试运行。每个TEST_F有两个参数,测试类名称和测试名称(应该非常描述性...这样如果测试失败,你就知道哪里出了问题)。你会看到每个测试的结构都很相似:(1)设置一些初始内容,(2)运行你正在测试的方法,(3)验证输出是否正确。你检查(3)的方式是使用像EXPECT_*这样的宏。
EXPECT_EQ(expected, result)
检查
result
是否等于
expected
。如果不是,你会得到一个有用的错误消息,例如“结果是blah,但期望是Blah”。
以下是代码(希望这不会太令人困惑...它肯定不是一个简短或容易的例子,但如果你花时间,应该能够理解它的一般工作原理)。
#include <fstream>
#include <iostream>
#include <gtest/gtest.h>
#include <sstream>
#include <string>
#include <vector>
#include "iloc_parser.h"
using namespace std;
namespace compilers {
class IlocParserTest : public testing::Test {
protected:
IlocParserTest() {}
virtual ~IlocParserTest() {}
virtual void SetUp() {
const testing::TestInfo* const test_info =
testing::UnitTest::GetInstance()->current_test_info();
test_name_ = test_info->name();
}
string test_name_;
};
static void ReadFileAsString(const string& filename, string* output) {
ifstream in_file(filename.c_str());
stringstream result("");
string temp;
while (getline(in_file, temp)) {
result << temp << endl;
}
*output = result.str();
}
TEST_F(IlocParserTest, ReplaceSingleInstanceOfSingleCharWithEmptyString) {
string to_replace = "blah,blah";
string to_find = ",";
string replace_with = "";
IlocParser::FindAndReplace(to_find, replace_with, &to_replace);
EXPECT_EQ("blahblah", to_replace);
}
TEST_F(IlocParserTest, ReplaceMultipleInstancesOfSingleCharWithEmptyString) {
string to_replace = "blah,blah,blah";
string to_find = ",";
string replace_with = "";
IlocParser::FindAndReplace(to_find, replace_with, &to_replace);
EXPECT_EQ("blahblahblah", to_replace);
}
TEST_F(IlocParserTest,
ReplaceMultipleInstancesOfMultipleCharsWithEmptyString) {
string to_replace = "blah=>blah=>blah";
string to_find = "=>";
string replace_with = "";
IlocParser::FindAndReplace(to_find, replace_with, &to_replace);
EXPECT_EQ("blahblahblah", to_replace);
}
TEST_F(IlocParserTest, StripIlocLineLoadI) {
string iloc_line = "loadI\t1028\t=> r11";
IlocParser::StripIlocLine(&iloc_line);
EXPECT_EQ("loadI\t1028\t 11", iloc_line);
}
TEST_F(IlocParserTest, StripIlocLineSubWithComment) {
string iloc_line = "sub\tr12, r10\t=> r13 // Subtract r10 from r12\n";
IlocParser::StripIlocLine(&iloc_line);
EXPECT_EQ("sub\t12 10\t 13 ", iloc_line);
}
TEST_F(IlocParserTest, TokenizeIlocLineNormalInstruction) {
string iloc_line = "sub\t12 10\t 13\n";
vector<string> tokens;
IlocParser::TokenizeIlocLine(iloc_line, &tokens);
EXPECT_EQ(4, tokens.size());
EXPECT_EQ("sub", tokens[0]);
EXPECT_EQ("12", tokens[1]);
EXPECT_EQ("10", tokens[2]);
EXPECT_EQ("13", tokens[3]);
}
TEST_F(IlocParserTest, CreateIlocInstructionLoadI) {
vector<string> tokens;
tokens.push_back("loadI");
tokens.push_back("1");
tokens.push_back("5");
IlocInstruction instruction(IlocInstruction::NONE);
EXPECT_TRUE(IlocParser::CreateIlocInstruction(tokens,
&instruction));
EXPECT_EQ(IlocInstruction::LOADI, instruction.op_code());
EXPECT_EQ(2, instruction.num_operands());
IlocInstruction::OperandList::const_iterator it = instruction.begin();
EXPECT_EQ(1, *it);
++it;
EXPECT_EQ(5, *it);
}
TEST_F(IlocParserTest, CreateIlocInstructionFromMisspelledOp) {
vector<string> tokens;
tokens.push_back("ADD");
tokens.push_back("1");
tokens.push_back("5");
tokens.push_back("2");
IlocInstruction instruction(IlocInstruction::NONE);
EXPECT_FALSE(IlocParser::CreateIlocInstruction(tokens,
&instruction));
EXPECT_EQ(0, instruction.num_operands());
}
TEST_F(IlocParserTest, CreateIlocInstructionFromNoTokens) {
vector<string> tokens;
IlocInstruction instruction(IlocInstruction::NONE);
EXPECT_TRUE(IlocParser::CreateIlocInstruction(tokens,
&instruction));
EXPECT_EQ(IlocInstruction::NONE, instruction.op_code());
EXPECT_EQ(0, instruction.num_operands());
}
TEST_F(IlocParserTest, MakeIlocLineFromInstructionAddI) {
IlocInstruction instruction(IlocInstruction::ADDI);
vector<int> operands;
operands.push_back(1);
operands.push_back(2);
operands.push_back(3);
instruction.CopyOperandsFrom(operands);
string output;
EXPECT_TRUE(IlocParser::MakeIlocLineFromInstruction(instruction, &output));
EXPECT_EQ("addI r1, 2 => r3", output);
}
TEST_F(IlocParserTest, ParseIlocFileSimple) {
IlocParser parser;
vector<IlocInstruction*> lines;
EXPECT_TRUE(parser.ParseIlocFile(test_name_, &lines));
EXPECT_EQ(2, lines.size());
EXPECT_EQ(IlocInstruction::ADD, lines[0]->op_code());
EXPECT_EQ(3, lines[0]->num_operands());
IlocInstruction::OperandList::const_iterator operand = lines[0]->begin();
EXPECT_EQ(1, *operand);
++operand;
EXPECT_EQ(2, *operand);
++operand;
EXPECT_EQ(3, *operand);
EXPECT_EQ(IlocInstruction::LOADI, lines[1]->op_code());
EXPECT_EQ(2, lines[1]->num_operands());
operand = lines[1]->begin();
EXPECT_EQ(5, *operand);
++operand;
EXPECT_EQ(10, *operand);
for (vector<IlocInstruction*>::iterator it = lines.begin();
it != lines.end();
++it) {
delete *it;
}
}
TEST_F(IlocParserTest, WriteIlocFileSimple) {
IlocInstruction instruction1(IlocInstruction::ADD);
vector<int> operands;
operands.push_back(1);
operands.push_back(2);
operands.push_back(3);
instruction1.CopyOperandsFrom(operands);
operands.clear();
IlocInstruction instruction2(IlocInstruction::LOADI);
operands.push_back(17);
operands.push_back(10);
instruction2.CopyOperandsFrom(operands);
operands.clear();
IlocInstruction instruction3(IlocInstruction::OUTPUT);
operands.push_back(1024);
instruction3.CopyOperandsFrom(operands);
vector<IlocInstruction*> lines;
lines.push_back(&instruction1);
lines.push_back(&instruction2);
lines.push_back(&instruction3);
string out_filename = test_name_ + "_output";
string golden_filename = test_name_ + "_golden";
IlocParser parser;
EXPECT_TRUE(parser.WriteIlocFile(out_filename, lines));
string golden_file;
string out_file;
ReadFileAsString(golden_filename, &golden_file);
ReadFileAsString(out_filename, &out_file);
EXPECT_EQ(golden_file, out_file);
}
}
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
说了这么多...我为什么要这样做呢?首先,我在准备编写每个代码片段时逐步编写测试。这有助于让我放心已经编写的代码正常工作。如果我只是在文件上尝试运行所有代码并查看发生了什么,那将是疯狂的。有这么多层,除非我对每个小片段进行隔离测试,否则我怎么知道错误会从哪里出现呢?
但是...最重要的是!测试实际上并不是为了捕获代码中的初始错误...它是为了保护自己免受意外破坏代码的影响。每次我重构或更改我的IlocParser类时,我都很有信心没有以错误的方式更改它,因为我可以运行我的测试(在几秒钟内)并查看所有代码是否仍按预期工作。这就是单元测试的伟大用处。
它们似乎需要太多时间...但最终,它们会节省您追踪错误所需的时间,因为您更改了一些代码并不知道发生了什么。它们是验证小代码片段是否按预期执行的有用方法。