什么是单元测试?

7

可能是重复问题:
什么是单元测试,如何进行单元测试?
什么是单元测试?

我知道对于95%的人来说,这是一个非常奇怪的问题。

那么,什么是单元测试?我知道你基本上正在尝试隔离原子功能,但是你如何测试呢?什么时候需要进行单元测试?什么时候是荒谬的? 你能举个例子吗?(最好用C语言?我在这个网站上主要听到Java开发人员的讨论,所以也许这是特定于面向对象语言的?我真的不知道。)

我知道很多程序员都信奉单元测试。这是怎么回事?

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


@womp:我刚刚搜索了一下,没有看到任何类似标题的内容。 - Spencer Ruport
2
http://stackoverflow.com/search?q=unit+testing - Nifle
2
http://stackoverflow.com/search?q=What+unit+test 返回了很多结果,其中包括https://dev59.com/AXVD5IYBdhLWcg3wXaid等20页的内容。 - John Saunders
实际例子,Roy Osherove评论了NerdDinner单元测试 http://www.viddler.com/explore/RoyOsherove/videos/4/723.2/ - Nathan Koop
顺便说一下,我很惊讶我不得不删除“'s”和“a”。 - John Saunders
显示剩余5条评论
7个回答

9
我现在用Java,以前用C++,更早之前用C。我完全相信我所做的每一件工作都受益于我采用的测试策略。省略测试会有害无益。
我相信你会测试你编写的代码。你使用什么技术?例如,你可能坐在调试器中逐步执行代码并观察发生了什么。你可能根据某些人提供的测试数据执行代码。你可能设计特定的输入,因为你知道你的代码对某些输入值具有一些有趣的行为。假设你的东西使用别人的东西,但那不准备好了,你可以模拟他们的代码,这样你的代码至少可以使用一些虚假的答案。
在所有情况下,你可能会在某种程度上进行单元测试。最后一个特别有趣-即使他们的代码尚未准备好,你也非常独立地进行测试,测试你的单位。
我的意见:
1)易于重新运行的测试非常有用-捕捉到了许多晚期潜在缺陷。相比之下,在调试器中进行测试是令人昏昏欲睡的。
2)在编写代码或编写代码之前构建有趣的测试活动使您专注于边缘情况。那些恼人的零和空输入,那些“偏离一个错误”。我认为好的单元测试会产生更好的代码。
3)维护测试的成本很高。通常它是值得的,但不要低估保持它们工作的努力。
4)有一种过度强调单元测试的倾向。当各个部分集成时,真正有趣的错误往往会悄然而至。你用真正的东西替换了那个库,但是哎呀!它并没有完全像标签上说的那样做。此外,手动或探索性测试仍然有其作用。有洞察力的人类测试人员可以发现特殊缺陷。

说得好。也许你还可以补充一下“你现在如何测试”的内容:你只需添加(太多)日志并在重新运行应用程序时阅读它,甚至每次点击/浏览远离应用程序的地方,在查看是否按预期工作之前交互式设置正确的数据。(断言预期实际:-)) - raoulsson

2
我能为您提供的最简单/非技术性的定义是自动化测试代码部分的方法...
我使用并且喜欢它...但并不是教条式的。在单元测试中,我最自豪的时刻是一个我为一家银行做的利息计算,非常复杂,但只有一个bug,而那种情况下没有单元测试...当我添加了这个情况并修复了我的代码之后,它就完美了。
所以,以此为例,我有一个名为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(),以便在调试模式下手动检查我的结果。


1

单元测试是您编写的另一段软件,用于测试您的主要代码是否符合所需功能。

我可以编写一个计算器程序,它看起来很好,有按钮,看起来像TI-whatever计算器,并且可以产生2+2=5。看起来不错,但与其将某些代码的每个迭代发送给人类测试人员,让他们进行长列表检查,不如我作为开发人员在我的代码上运行一些自动化编码的单元测试。

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

单元测试将具有一组“给定值”或“输入”,并将这些与预期的“输出”进行比较。

当然,关于何时、如何以及使用多少单元测试存在不同的方法论(请查看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;
    }
}

你假设减速福特塔瑞斯必须传递一个负值。然而,假设你有一组针对汽车接口编写的单元测试,它们看起来像这样:

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

测试告诉你,也许你实现了汽车接口错误。

你会对你的代码进行单元测试吗?对于你编写的每个函数的每个可能参数都进行测试吗?编写单元测试所花费的时间与编写新代码所花费的时间之比是多少? - Tyler
1
简短回答:这取决于情况。请查看其他SO答案以了解更多信息。 如果我不需要也不想,我就不会写测试。 有时候我是必须要写的。 有时候我觉得写测试会很有帮助。 但有时候写测试就显得有些荒谬了。 许多人会告诉你,如果你的代码需要传递、重新处理或重构,那么可能需要一组测试。 - maxwellb
1
回答关于编写测试和编写新代码所花费时间比例的问题,意味着采用特定的编写代码方法论。如果你还在上高中,只是想获得编程经验,那么请了解采用任何方法都需要时间和计划。如果你想练习TDD,请获取一本相关书籍,并逐章阅读,例如每周一章(注意:我不知道有哪些专门讲述此主题的书籍)。问问自己:“我的目标是什么?”如果你正在使用特定的API,深入测试可能不是答案。 :-) - maxwellb

1

你想要例子吗?上学期我修了一门编译器课程。在这门课程中,我们必须编写一个寄存器分配器。简单来说,我的程序可以总结如下:

输入:一个以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”。
以下是代码(希望这不会太令人困惑...它肯定不是一个简短或容易的例子,但如果你花时间,应该能够理解它的一般工作原理)。
// 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类时,我都很有信心没有以错误的方式更改它,因为我可以运行我的测试(在几秒钟内)并查看所有代码是否仍按预期工作。这就是单元测试的伟大用处。
它们似乎需要太多时间...但最终,它们会节省您追踪错误所需的时间,因为您更改了一些代码并不知道发生了什么。它们是验证小代码片段是否按预期执行的有用方法。

哇,谢谢你把这个整理好。这非常非常有帮助。 - Tyler

1
点对点:
1)什么是单元测试?
单元测试是一种软件测试,旨在测试软件的一个独立功能单元。
2)我理解你的目标是隔离原子功能,但如何进行测试?
单元测试实际上是强制执行某些设计原则的好方法;其中一个方面是它们实际上对代码设计产生了微妙但重要的影响。为测试而设计是一件重要的事情;能够测试(或不测试)某个代码片段可能非常重要;当使用单元测试时,设计往往会朝着“更具原子性”的一侧移动。
3)何时需要进行单元测试?
对此观点各异。有人说总是需要进行单元测试,有人说完全不必要。我认为,大多数具有单元测试经验的开发人员都会认为单元测试对于任何具有适合进行单元测试设计的关键路径代码都是必要的(虽然有点循环,但参见上面的问题#2)。
4)何时荒谬?你能举个例子吗?

通常,过度测试是指你进入了谷底。例如,如果你有一个3D向量类,它有每个标量分量的访问器,为每个标量访问器编写单元测试来确认完整的输入范围并验证每个值可能被一些人认为是有点过头了。另一方面,需要注意的是,即使在这种情况下,测试也可能是有用的。

  1. 我大多数时候在这个网站上听到Java开发人员提到它,所以这可能是特定于面向对象语言吗?

不,它确实适用于任何软件。单元测试方法论在Java环境中成熟,但它确实适用于任何语言或环境。

  1. 这是关于什么的?

单元测试在非常基本的层面上,就是验证和验证代码单元预期行为是否与代码实际执行的行为相符。


0
在计算机编程中,单元测试是一种软件验证和验证方法,程序员通过测试源代码的各个单元来验证其适用性。一个单元是应用程序中最小的可测试部分。在过程式编程中,一个单元可以是一个独立的程序、函数、过程等;而在面向对象编程中,最小的单元是一个类,它可以属于基类/超类、抽象类或派生/子类。

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


这相当无礼。当然,我已经查看了关于单元测试的维基百科文章。但我的问题并没有得到答案。我想要一些来自实际世界的例子。我知道这是一个很大的问题,我想知道它在实践中如何使用。 - Tyler
你也读了那篇文章中的外部链接吗? - Nifle
@Nifle是的,又是没有真实世界的见解,只有理论。 - Tyler
我不认为这是无礼的。你问了一个非常基础的软件问题 - 我向你指出了一个很好的参考资料。 - ist_lion
阅读 http://en.wikipedia.org/wiki/Recursion_(computer_science) 并告诉我它概述了递归在现实世界中的缺陷。 - Tyler
我想帮你,但是你给了我一个错误的URL...所以不仅你在单元测试中失败了 - 你甚至不能在参数中提供真实的URL。 - ist_lion

0
例如,如果您有一个矩阵类,您可能会编写一个单元测试来检查:
Matrix A = Matrix(.....); A.inverse()*A ==Matrix::Identity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接