Итак, вы хотите примеры? В прошлом семестре я взял курс компиляторов. В ней нам пришлось написать распределитель регистров. Проще говоря, мою программу можно резюмировать следующим образом:
Ввод: Файл, написанный на ILOC, языке псевдоассоциирования, который был составлен для моего учебника. Инструкции в файле имеют имена регистров типа «r <number>
». Проблема заключается в том, что в программе используется столько регистров, сколько требуется, что обычно больше, чем количество регистров на целевой машине.
Выход: Другой файл, написанный в ILOC. На этот раз инструкции переписаны так, что они используют правильное максимальное количество регистров, которые разрешены.
Чтобы написать эту программу, мне пришлось создать класс, который мог бы анализировать файл ILOC. Я написал кучу тестов для этого класса. Ниже приведены мои тесты (на самом деле у меня было больше, но они избавились от них, чтобы помочь сократить это. Я также добавил некоторые комментарии, чтобы помочь вам прочитать его). Я сделал проект на C++, поэтому я использовал тестовую среду Google C++ (googletest), расположенную here.
Прежде чем показывать вам код ... позвольте мне сказать кое-что о базовой структуре. По существу, есть тестовый класс. Вы можете добавить кучу общих настроек в тестовый класс. Затем есть тестовые макросы, называемые TEST_F. Рамки тестирования подхватывают их и понимают, что их нужно запускать в качестве тестов. Каждый TEST_F имеет 2 аргумента, имя тестового класса и имя теста (который должен быть очень описательным ... таким образом, если тест не удался, вы точно знаете, что не удалось).Вы увидите, что структура каждого теста схожа: (1) настроить некоторые исходные данные, (2) запустить метод, который вы тестируете, (3) проверить правильность вывода. То, как вы проверяете (3), - это использовать макросы, такие как EXPECT_ *. EXPECT_EQ(expected, result)
проверяет, что result
равно expected
. Если это не так, вы получите полезное сообщение об ошибке вроде «результат был бла, но ожидаемый Бла».
Вот код (я надеюсь, что это не очень запутывает ... это, конечно, не короткий или простой пример, но если вы нашли время, вы должны быть в состоянии следовать и получить общий вкус, как это работает).
// Unit tests for the iloc_parser.{h, cc}
#include <fstream>
#include <iostream>
#include <gtest/gtest.h>
#include <sstream>
#include <string>
#include <vector>
#include "iloc_parser.h"
using namespace std;
namespace compilers {
// Here is my test class
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_;
};
// Here is a utility function to help me test
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();
}
// All of these TEST_F things are macros that are part of the test framework I used.
// Just think of them as test functions. The argument is the name of the test class.
// The second one is the name of the test (A descriptive name so you know what it is
// testing).
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);
}
// This test was suppsoed to strip out the "r" from register
// register names in the ILOC code.
TEST_F(IlocParserTest, StripIlocLineLoadI) {
string iloc_line = "loadI\t1028\t=> r11";
IlocParser::StripIlocLine(&iloc_line);
EXPECT_EQ("loadI\t1028\t 11", iloc_line);
}
// Here I make sure stripping the line works when it has a comment
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);
}
// Here I make sure I can break a line up into the tokens I wanted.
TEST_F(IlocParserTest, TokenizeIlocLineNormalInstruction) {
string iloc_line = "sub\t12 10\t 13\n"; // already stripped
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]);
}
// Here I make sure I can create an instruction from the tokens
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);
}
// Making sure the CreateIlocInstruction() method fails when it should.
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());
}
// Make sure creating an empty instruction works because there
// were times when I would actually have an empty tokens vector.
TEST_F(IlocParserTest, CreateIlocInstructionFromNoTokens) {
// Empty, which happens from a line that is a comment.
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());
}
// This was a function that helped me generate actual code
// that I could output as a line in my output file.
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);
}
// This test actually glued a bunch of stuff together. It actually
// read an input file (that was the name of the test) and parsed it
// I then checked that it parsed it correctly.
TEST_F(IlocParserTest, ParseIlocFileSimple) {
IlocParser parser;
vector<IlocInstruction*> lines;
EXPECT_TRUE(parser.ParseIlocFile(test_name_, &lines));
EXPECT_EQ(2, lines.size());
// Check first line
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);
// Check second line
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);
// Deallocate memory
for (vector<IlocInstruction*>::iterator it = lines.begin();
it != lines.end();
++it) {
delete *it;
}
}
// This test made sure I generated an output file correctly.
// I built the file as an in memory representation, and then
// output it. I had a "golden file" that was supposed to represent
// the correct output. I compare my output to the golden file to
// make sure it was correct.
TEST_F(IlocParserTest, WriteIlocFileSimple) {
// Setup instructions
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);
// Propogate lines with the instructions
vector<IlocInstruction*> lines;
lines.push_back(&instruction1);
lines.push_back(&instruction2);
lines.push_back(&instruction3);
// Write out the file
string out_filename = test_name_ + "_output";
string golden_filename = test_name_ + "_golden";
IlocParser parser;
EXPECT_TRUE(parser.WriteIlocFile(out_filename, lines));
// Read back output file and verify contents are as expected.
string golden_file;
string out_file;
ReadFileAsString(golden_filename, &golden_file);
ReadFileAsString(out_filename, &out_file);
EXPECT_EQ(golden_file, out_file);
}
} // namespace compilers
int main(int argc, char** argv) {
// Boiler plate, test initialization
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
В конце концов, сказано и сделано ... ПОЧЕМУ Я СДЕЛАЛ ЭТО !? Ну в первую очередь. Я написал тесты постепенно, поскольку я готов написать каждый фрагмент кода. Это помогло мне успокоиться, что код, который я уже писал, работает нормально. Было бы безумным написать весь мой код, а затем просто попробовать его в файле и посмотреть, что произошло. Было так много слоев, как я мог знать, откуда возникла ошибка, если бы у меня не было каждой частичной штуки в изоляции?
НО ... САМЫЙ ВАЖНО !!! Тестирование на самом деле не связано с поиском исходных ошибок в вашем коде ... это защита от случайного нарушения вашего кода. Каждый раз, когда я реорганизовал или изменил свой класс IlocParser, я был уверен, что я не изменил его плохо, потому что я мог запускать свои тесты (в считанные секунды) и видеть, что весь код все еще работает, как ожидалось. Это большое использование модульных тестов.
Кажется, что они занимают слишком много времени ... но в конечном итоге они сэкономит вам время на отслеживание ошибок, потому что вы изменили код и не знаете, что произошло. Они являются полезным способом проверки того, что маленькие кусочки кода делают то, что они должны делать, и правильно.
Дубликат дубликатов. Просто найдите переполнение стека для «Что такое единичный тест», и вам будет достаточно, чтобы пережевывать в течение нескольких месяцев. – womp
@womp: Я только что сделал, и я не видел никаких похожих заголовков. –
http://stackoverflow.com/search?q=unit+testing – Nifle