什么是单元测试?

时间:2009-07-13 21:32:47

标签: unit-testing

  

可能重复:
  What is unit testing and how do you do it?
  What is unit testing?

我认识到95%的人,这是一个非常WTF的问题。

因此。什么是单元测试?我明白,基本上你是在尝试隔离原子功能,但你是如何进行测试的?什么时候需要?什么时候荒谬? 你能给我举个例子吗? (最好是在C?我主要是从这个网站上的Java开发者那里听到的,所以这可能是针对面向对象语言的?我真的不知道。)

我知道很多程序员都在虔诚地进行单元测试。这是怎么回事?

编辑:此外,您通常花在编写单元测试上的时间与编写新代码所花费的时间之比是多少?

7 个答案:

答案 0 :(得分:9)

我现在是Java,在那个C ++之前,在那之前C.我完全相信我所做的每件工作,我现在都不感到羞耻,因为我选择的测试策略得到了增强。吝啬测试伤害。

我确定你测试了你编写的代码。你用什么技术?例如,您可能会坐在调试器中并逐步执行代码并观察发生的情况。您可以针对某人给您的某些测试数据执行代码。您可能会设计特定的输入,因为您知道您的代码对某些输入值有一些有趣的行为。假设你的东西使用别人的东西而且还没准备好,你模拟他们的代码,这样你的代码至少可以使用一些假的答案

在所有情况下,您可能在某种程度上进行单元测试。最后一个特别有趣 - 你正在进行单独的测试,测试你的UNIT,即使它们还没有准备好。

我的意见:

1)。可以轻松重新运行的测试非常有用 - 捕捉到晚期爬行缺陷的结束。 相比之下,坐在调试器中的测试令人头脑麻木。

2)。在编写代码时构建有趣测试的活动,或者在编写代码之前,您可以专注于边缘情况。那些烦人的零和空输入,那些“一个错误”。由于良好的单元测试,我认为更好的代码会出现。

3)。维护测试需要付出代价。一般来说它是值得的,但不要低估让它们保持工作的效率。

4)。可能存在过度单元测试的趋势。当整合件时,真正有趣的错误往往会蔓延。你用真实的东西替换你嘲笑的那个库和Lo!它并没有完全按照它在锡上说的那样做。此外,还有手动或探索性测试的作用。富有洞察力的人类测试人员发现了特殊缺陷。

答案 1 :(得分:2)

最简单/非技术定义我可以提出自动测试代码部分的方法 ......

我使用它并且喜欢它...但不是虔诚地,单位测试中我最自豪的一个时刻是我为银行做的利息计算,非常复杂,我只有一个错误而且没有单元测试那个案例......一旦我添加了案例并修复了我的代码,它就完美了。

所以拿这个例子我有一个类调用InterestCalculation,它有所有参数的属性和一个公共方法Calculate()那里有几个计算步骤,如果我在哪里尝试和用一个方法写完整个东西,然后查看我的结果,试图找到我的bug / s所在的位置是不堪重负的......所以我采取了计算的每一步并创建了一个私有方法和一个单元测试/ s对于所有不同的情况。 (有些人会告诉你只测试公共方法,但在这种情况下,它对我来说效果更好......)私有方法的一个例子是:

方式:

    /// <summary>
    /// 
    /// </summary>
    /// <param name="effectiveDate"></param>
    /// <param name="lastCouponDate"></param>
    /// <returns></returns>
    private Int32 CalculateNumberDaysSinceLastCouponDate(DateTime effectiveDate, DateTime lastCouponDate)
    {
        Int32 result = 0;

        if (lastCouponDate.Month == effectiveDate.Month)
        {
            result = this._Parameters.DayCount.GetDayOfMonth(effectiveDate) - lastCouponDate.Day;
        }
        else
        {
            result = this._Parameters.DayCount.GetNumberOfDaysInMonth(lastCouponDate)
                - lastCouponDate.Day + effectiveDate.Day;
        }

        return result;
    }

测试方法:

  

注意:我现在用不同的名字命名,而不是我想要的数字   基本上把摘要放入了   方法名称。

    /// <summary>
    ///A test for CalculateNumberDaysSinceLastCouponDate
    ///</summary>
    [TestMethod()]
    [DeploymentItem("WATrust.CAPS.DataAccess.dll")]
    public void CalculateNumberDaysSinceLastCouponDateTest1()
    {
        AccruedInterestCalculationMonthly_Accessor target = new AccruedInterestCalculationMonthly_Accessor();
        target._Parameters = new AccruedInterestCalculationMonthlyParameters();
        target._Parameters.DayCount = new DayCount(13);
        DateTime effectiveDate = DateTime.Parse("04/22/2008");
        DateTime lastCouponDate = DateTime.Parse("04/15/2008");
        int expected = 7;
        int actual;

        actual = target.CalculateNumberDaysSinceLastCouponDate(effectiveDate, lastCouponDate);

        Assert.AreEqual(expected, actual);

        WriteToConsole(expected, actual);
    }

    /// <summary>
    ///A test for CalculateNumberDaysSinceLastCouponDate
    ///</summary>
    [TestMethod()]
    [DeploymentItem("WATrust.CAPS.DataAccess.dll")]
    public void CalculateNumberDaysSinceLastCouponDateTest2()
    {
        AccruedInterestCalculationMonthly_Accessor target = new AccruedInterestCalculationMonthly_Accessor();
        target._Parameters = new AccruedInterestCalculationMonthlyParameters();
        target._Parameters.DayCount = new DayCount((Int32)
            DayCount.DayCountTypes.ThirtyOverThreeSixty);

        DateTime effectiveDate = DateTime.Parse("04/10/2008");
        DateTime lastCouponDate = DateTime.Parse("03/15/2008");
        int expected = 25;
        int actual;

        actual = target.CalculateNumberDaysSinceLastCouponDate(effectiveDate, lastCouponDate);

        Assert.AreEqual(expected, actual);

        WriteToConsole(expected, actual);
    }            

哪里可笑?

对每个人来说都很好......你做的越多,你会发现它有用的地方以及它看起来“荒谬”的地方但个人而言,我不会用它来测试我的数据库中的大多数铁杆单元测试人员......从某种意义上说,我有一个脚本来重建数据库模式,用测试数据等重新填充数据库。我通常编写一个单元测试方法来调用我的DataAccess方法并用一个Debug后缀标记它,就像这样:FindLoanNotes_Debug(),我一直在使用System.Diagnostics.Debugger.Break(),所以如果我在调试模式下运行它,我可以手动检查我的结果。

答案 2 :(得分:1)

单元测试是您编写的另一个软件,用于练习您的主要代码以接受所需的功能。

我可以编写一个看起来不错的计算器程序,有按钮,看起来像TI无论计算器,它可以产生2 + 2 = 5。看起来不错,但不是将一些代码的每次迭代发送给人工测试人员,而是通过一长串检查,我,开发人员可以对我的代码运行一些自动编码的单元测试。

基本上,单元测试应该由自己,同行或其他仔细审查进行测试,以回答“这是我想要的测试吗?”

单元测试将有一组“Givens”或“Inputs”,并将这些与预期的“输出”进行比较。

当然,关于如何,何时以及使用单元测试的方法有不同的方法(在这些方面检查一些问题的SO)。但是,在最基本的情况下,它们是一个程序,或者是其他程序的可加载模块,它使断言

单元测试的标准语法可能是有一行代码如下:Assert.AreEqual( a, b )

单元测试方法体可以设置输入和实际输出,并将其与预期输出进行比较。

HelloWorldExample helloWorld = new HelloWorldExample();
string expected = "Hello World!";
string actual = helloWorld.GetString();

Assert.AreEqual( expected, actual );

如果您的单元测试是使用特定框架的语言编写的(例如 jUnit,NUnit等),那么标记为“测试运行”一部分的每种方法的结果将被汇总成一组测试结果,例如失败的红点和成功的绿点,和/或XML文件等的漂亮图表。

根据您的最新评论,“理论”可以提供一些真实世界的见解。 TDD,测试驱动开发,说了很多关于何时以及多久使用一次测试。在我的最新项目中,我们没有遵守TDD,但我们确实使用了单元测试来验证我们的代码是否完成了它应该做的事情。

假设您已选择实施Car界面。 Car界面如下所示:

interface ICar
{
    public void Accelerate( int delta );
    public void Decelerate( int delta );
    public int GetCurrentSpeed();
}

您选择在FordTaurus类中实现Car接口:

class FordTaurus : ICar
{
    private int mySpeed;
    public Accelerate( int delta )
    {
        mySpeed += delta;
    }
    public Decelerate( int delta )
    {
        mySpeed += delta;
    }
    public int GetCurrentSpeed()
    {
        return mySpeed;
    }
}

你假设要减速FordTaurus,必须传递负值。但是,假设您有一组针对Car接口编写的单元测试,它们看起来像这样:

public static void TestAcceleration( ICar car )
{
    int oldSpeed = car.GetCurrentSpeed();
    car.Accelerate( 5 );
    int newSpeed = car.GetCurrentSpeed();
    Assert.IsTrue( newSpeed > oldSpeed );
}
public static void TestDeceleration( ICar car )
{
    int oldSpeed = car.GetCurrentSpeed();
    car.Decelerate( 5 );
    int newSpeed = car.GetCurrentSpeed();
    Assert.IsTrue( newSpeed < oldSpeed );
}

测试告诉您,您可能错误地实施了汽车界面。

答案 3 :(得分:1)

逐点:

1)什么是单元测试?

单元测试是一种软件测试,旨在测试一个不同的软件功能单元。

2)我明白你基本上是在尝试隔离原子功能,但是你如何测试呢?

单元测试实际上是强制执行某些设计原则的好方法;它们的一个方面是它们实际上对代码的设计产生了微妙但重要的影响。设计测试是一件重要的事情;能够测试(或不测试)某些代码是非常重要的;当使用单元测试时,设计倾向于向频谱的“更原子”方向迁移。

3)什么时候需要?

对此有很多不同意见。有人说这总是必要的,有人说完全没必要。我认为大多数具有单元测试经验的开发人员会说单元测试对于任何具有适合单元测试的设计的关键路径代码是必要的(我知道它有点循环,但请参见上面的#2)。 / p>

  1. 什么时候可笑?你能给我举个例子吗?
  2. 一般来说,超越是你进入光谱的荒谬结局的地方。例如,如果您有一个3D Vector类,其中包含每个标量组件的访问器,则对每个标量访问器进行单元测试以确认输入的完整范围并验证每个标量访问器的值将被视为有点有些人过度杀戮另一方面,重要的是要注意即使这些情况也可用于测试。

    1. 我主要是从这个网站上的Java开发者那里听到它,所以这可能是针对面向对象语言的?
    2. 不,它真的适用于任何软件。单元测试方法在Java环境中逐渐成熟,但它确实适用于任何语言或环境。

      1. 这是怎么回事?
      2. 单元测试在一个非常基础的层面上,所有关于验证和验证代码单元所期望的行为实际上是代码所执行的行为。

答案 4 :(得分:1)

所以你想要例子吗?上学期我参加了一个编译器课程。在其中我们必须编写一个寄存器分配器。简单来说,我的程序可以概括为:

输入:用ILOC编写的文件,这是一种为我的教科书编写的伪汇编语言。文件中的指令具有“r <number>”之类的寄存器名称。问题是程序使用尽可能多的寄存器,这通常大于目标机器上的寄存器数量。

输出:另一个用ILOC编写的文件。这次,重写指令,使其使用允许的正确最大寄存器数。

为了编写这个程序,我必须创建一个可以解析ILOC文件的类。我为那堂课写了一堆测试。下面是我的测试(我实际上有更多,但摆脱它们来帮助缩短它。我还添加了一些注释来帮助你阅读它)。我用C ++完成了这个项目,所以我使用位于here的Google的C ++测试框架(googletest)。

在向您展示代码之前......让我谈谈基本结构。基本上,有一个测试类。你可以在测试类中放入一堆常规设置。然后是测试宏,称为TEST_F。测试框架了解了这些并理解它们需要作为测试运行。每个TEST_F都有2个参数,测试类名称和测试名称(应该是非常具有描述性的......如果测试失败,那么你确切地知道失败了)。您将看到每个测试的结构类似:(1)设置一些初始内容,(2)运行您正在测试的方法,(3)验证输出是否正确。检查(3)的方法是使用像EXPECT_ *这样的宏。 EXPECT_EQ(expected, result)检查result是否等于expected。如果不是,则会收到一条有用的错误消息,例如“结果是等等,但预计会有Blah”。

这是代码(我希望这不是一个非常令人困惑的......它肯定不是一个简短或简单的例子,但如果你花时间你应该能够跟随并获得它的工作原理的一般风格)。

// 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();
}

毕竟说完了......为什么我这样做!?首先,好吧。当我准备编写每段代码时,我逐步编写了测试。它让我安心,我已编写的代码正常工作。编写我的所有代码然后只是在文件上尝试并查看发生的情况本来是疯了。有这么多层,我怎么知道一个bug会来自哪里,除非我把每个小块分开测试?

但是......最重要!测试并不是关于捕获代码中的初始错误......而是关于保护自己免于意外破坏代码。每次我重构或改变我的IlocParser类时,我都相信我没有以一种糟糕的方式改变它,因为我可以运行我的测试(在几秒钟内)并且看到所有代码仍然按预期工作。这是单元测试的很好用途。

他们似乎花了太多时间......但最终,他们可以节省您追踪错误的时间,因为您更改了一些代码并且不知道发生了什么。它们是一种有用的方法,用于验证小块代码是否正在执行它们应该执行的操作,并且正确。

答案 5 :(得分:0)

在计算机编程中,单元测试是一种软件验证和验证方法,程序员在其中测试各个源代码单元是否适合使用。单元是应用程序中最小的可测试部分。在过程编程中,单元可以是单独的程序,功能,过程等,而在面向对象的编程中,最小的单元是类,其可以属于基础/超类,抽象类或派生/子类。

http://en.wikipedia.org/wiki/Unit_testing

答案 6 :(得分:0)

例如,如果您有矩阵类,则可能需要进行单元测试检查

矩阵A =矩阵(.....); A.inverse()* A == Matrix :: Identity