【问题标题】:Phpunit, how to test if method does "nothing"?Phpunit,如何测试方法是否“无”?
【发布时间】:2021-11-02 17:58:30
【问题描述】:
class Testme()
{
    public function testMe ($a)
    {
        if ($a == 1)
        {
            throw new Exception ('YAY');
        }
    }
}

所以很容易测试它是否抛出异常

/**
 * @expectedException Exception
 */
public function test()
{
    new Testme(1);
}

但是如果它什么也没做呢?

public function test()
{
    new Testme(2);
 ?? ? ? ? ?
}

【问题讨论】:

  • 请不要默默地投票!我认为这是一个完全有效的问题,所以如果您不同意,请解释。 @OP:这是一个相关问题:stackoverflow.com/questions/27511593/…
  • 它可以返回一个值 - 但如果我的原始方法是“做”某事,而不是“给予”某事,我不想更改原始代码
  • 不确定你的意思是什么......相关问题中的解决方案是将函数调用包装在try-catch-block中的测试中。如果抛出异常,您将捕获它并手动使测试失败 ($this->fail()),否则只需创建一个通过测试的虚拟断言 ($this->assertTrue(true))。您不测试是否返回任何内容,您只需关注异常。
  • 只是为了明确说明:(目前)还没有内置的解决方案。有一个 GitHub issue thread 讨论细节:github.com/sebastianbergmann/phpunit-documentation/issues/171

标签: php phpunit


【解决方案1】:

场景

一个函数什么都不做有两种可能的情况:

场景1:没有return语句

您的函数什么都不做,因为您没有在其中执行操作,并且您没有在其中包含 return 关键字:

public function doNothing()
{
    // Do nothing.
}

场景2:有return语句

您的函数什么都不做,因为您没有在其中执行操作,并且您确实包含 return 关键字没有表示任何返回值:

public function doNothing()
{
    // Do nothing.
    return;
}

其他场景

我将省略案例以处理以下情况:

  1. 您不返回任何内容但执行可在其他对象上测试的重要操作的情况。在这种情况下,您必须对修改后对象的结果状态进行单元测试。

  2. 如果你什么都不做,只返回一些东西,那么你应该对返回值进行单元测试。

浏览 PHP 手册中的文档

对于第一种情况,PHP 手册记录了函数的计算表达式将为null。它在这里说:http://php.net/manual/en/functions.returning-values.php 在注释中:

如果省略返回值,将返回 NULL。

对于第二种情况,PHP 手册记录了函数的求值表达式也将是null。它在这里说:http://php.net/manual/en/function.return.php 在注释中:

如果没有提供参数,则必须省略括号并返回 NULL。 [...]

结论

因此,清楚地记录了“什么都不做”的函数的计算结果必然为null

如何测试一个什么都不做的函数

只要表达你的期望:

$this->assertNull( $sut->doNothing() );

通过这种方式你“锻炼”你的函数,你在它上面运行,使代码覆盖完成所有的行,并且你通过将其评估的null 值测试为表达式来“预期”“什么都没有发生”,记录在案。

如何测试一个什么都不做的构造函数

尽管如此,还是要测试构造函数……嗯……常识:构造函数的目的是什么?创建某种类型(类)的对象(实例),对吧?

所以...我更喜欢通过检查 $sut 是否已创建来开始 100% 的单元测试。这是我在编写新类的代码时编写的第一个测试。这是我什至在课程存在之前编写的测试。最后,这就是构造函数的用途。红条。然后我创建了这个类。绿条。

假设我有一个 Email 类,它接受一个字符串,并且只有在传递有效的电子邮件时才会创建,否则会抛出异常。这与您的问题非常相似。仅“允许创建”或“通过爆炸系统拒绝创建”的构造函数。

我通常会这样做:

//-------------------------------------------------//
// Tests                                           //
//-------------------------------------------------//

/** @dataProvider validEmailProvider **/
public function testCreationIsOfProperClass( string $email )
{
    $sut = $this->getSut( $validEmail );
    $this->assertInstanceOf( Email::class, $sut );
}

/** @dataProvider invalidEmailProvider **/
public function testCreationThrowsExceptionIfEmailIsInvalid( string $invalidEmail )
{
    $this->expectException( EmailException::class );
    $this->getSut( $invalidEmail );
}

//-------------------------------------------------//
// Data providers                                  //
//-------------------------------------------------//

public function validEmailProvider() : array
{
    return
    [
        [ 'alice@example.com' ],
        [ 'bob.with-several+symbols@subdomain.another.subdomain.example.verylongTLD' ],
    ]
}

public function invalidEmailProvider() : array
{
    return
    [
        [ 'missing_at_symbol' ],
        [ 'charlie@cannotBeOnlyTld' ],
    ]
}

//-------------------------------------------------//
// Sut creators                                    //
//-------------------------------------------------//

private function getSut( string $email ) : Email
{
    return new Email( $email );
}

由于我使用 PHP 7.0 并且我将类型放在任何地方,包括输入参数和返回类型,如果创建的对象不是电子邮件,getSut() 函数将首先失败。

但即使我写它省略了返回类型,测试也会测试预期会发生什么:new Email( 'valid@example.com' ); 本身就是一个表达式,它应该评估为 Email::class 类的“某事”。

如何测试做某事的构造函数

代码气味。构造函数可能不应该工作。如果有,只需存储参数。如果构造函数“确实有效”而不是存储参数,请考虑在 getter 上进行延迟处理,或者在工厂中委派该工作。

如何测试“除了存储参数什么都不做”的构造函数

就像之前一样 + 然后获取数据。

  1. 在您的第一个测试中测试创建是某物的实例。
  2. 然后在另一个不同的测试中,使用 getter 之类的东西来获取在构造函数中输入的内容,即使构造函数没有执行任何操作(除了存储它)。

希望这会有所帮助。

【讨论】:

  • 确实有效,但请注意,如果该方法使用 PHP 7.1“void”返回类型,它仍然有效,但许多静态分析工具会报告为“使用的无效方法结果” "。
  • 很好的答案,但是:“构造函数的目的是什么?创建一个特定类型的类,对吧?”实际上,构造函数实例化了某个类/类型的对象。
  • 是的,您的细微修正是正确的。构造函数不是“创建类”,而是“创建某个类的实例”。确实如此。我将编辑答案以纳入您的贡献。感谢您的贡献!
  • PHPUnit 7.1, 2018 不需要这种复杂的方法。请参阅下面的答案。
【解决方案2】:

在 PHPUnit 7.2+ 中你也可以使用TestCase::expectNotToPerformAssertions()

public function test()
{
    // ...

    $this->expectNotToPerformAssertions();
}

这与@doesNotPerformAssertions 注释具有相同的行为。

【讨论】:

  • 这个。我尝试不写任何断言,PHPUnit 将测试标记为有风险。这是正确的做法。
【解决方案3】:

2018+

现在最好的做法是针对这些情况进行注释:

/**
 * @doesNotPerformAssertions
 */
public function testSomething()
{
    $someService = new SomeObject();
    $someService->shallNotFail();
}

【讨论】:

  • 那里的文档 not 声明此注释是“用于测试方法什么都不做”,但是用于避免警告消息,即总是不好的做法。从字面上看,它说“@doesNotPerformAssertions 防止不执行断言的测试被认为是有风险的。”。问题不是问“我如何在不测试任何东西的情况下执行代码”但是“我如何测试它什么都不做”,这是不同的。将报告设为“静音”应仅在我们有部分编码时暂时禁用冗长,而不是作为最终行为。
  • 感谢您与我们联系。你的评论让我很难理解。如果我们只看问题中的输入代码(不重构它),@doesNotPerformAssertions 正是测试它的方法。
  • 在我的回答中,我在“如何测试不执行任何操作的构造函数”部分的问题中说明了源的情况。 new 运算符确实“做了”一些事情:创建给定类的新实例。也就是说,测试是$this->assertInstanceOf( HappyClass::class, new HappyClass( $whatever );
  • 这个问题是相当笼统的“Phpunit,如何测试方法是否“没有”?认为您的答案在技术上是正确的,来自谷歌的人会为自己的代码寻找答案。答案不仅应该帮助询问者的确切详细代码,还应该帮助他的所有追随者。 @doesNotPerformAssertions 站在那里
【解决方案4】:

这是不可能的。添加return 语句并断言结果。

class Testme()
{
    public function testMe ($a)
    {
        if ($a == 1)
        {
            throw new Exception ('YAY');
        }

        return true;
    }
}

然后

$object = new Testme();
$this->assertTrue($object->testMe(2));

【讨论】:

    【解决方案5】:

    注意:此解决方案的功劳归this related answer。上下文可能看起来有些不同,但解决方案/解决方法的工作方式相同。测试不抛出异常与测试没有返回值的方法是一样的。

    根据this issue thread,没有内置的解决方案可以在PHPUnit 中测试DoesNotThrowException 之类的东西(目前)。

    所以是的,一种解决方案是从您的方法中返回一些虚拟值,例如

    public function testMe ($a)
    {
        if ($a == 1) { throw new Exception ('YAY'); }
    
        return true;
    }
    

    然后在你的测试中断言它。但是如果你不想仅仅为了测试而改变代码,你可以解决它:

    public function testExceptionIsNotThrown()
    {
        try {
            new Testme(2);
        }
        catch(Exception $e) {
            /* An exception was thrown unexpectedly, so fail the test */
            $this->fail();
        }
    
        /* No exception was thrown, so just make a dummy assertion to pass the test */
        $this->assertTrue(true);
    }
    

    它可能看起来很老套而且不是很直观,但是if it's stupid but it works, it's not stupid

    【讨论】:

      【解决方案6】:

      这是一个非常有趣的问题,虽然写了很多答案,但似乎没有一个能正确回答这个问题,因为您已经使用课堂提问了,让我这样解释。

      请记住,您在类中创建的实例方法应该只有两个意图。

      1. 它可以改变类的状态(改变类属性,如私有变量)
      2. 它返回类的状态(getter)

      除此之外的任何东西都是没有意义的,除非它是一个静态方法。例如 如果你有这样的课

      class Foo {
      
         private $prop = null;
         public function fooMethod() {
            $this->prop = "string";
         }
         public function getProp() {
           return $this->prop;
         }
      }
      

      方法fooMethod()不返回任何东西,但是会影响类中$prop属性的状态,可以通过方法测试

      $this->assertNotNull( $instance->getProp() );
      

      因为你知道如果这个方法被运行,那么 prop $prop 应该会受到影响并且该变量的状态会被改变。

      其他场景:我的方法不会改变状态,也不会返回任何状态变量。

      那么方法是static。它不应该是实例方法,而静态方法通常有返回类型,因为它们不能影响类的状态,也不能返回状态变量。这限制了静态方法将结果存储在某处(除非您将它们存储为全局,否则不要这样做),因此它肯定应该返回一些输出。如果您不想返回输出,则可以考虑从静态方法返回布尔值。

      【讨论】:

        【解决方案7】:
        public function testThrowingException()
        {
            $this->expectException(Exception::class);
            $this->expectExceptionMessage('YAY');
            (new Testme())->testMe(1);
        }
        
        public function testNotThrowingException()
        {
            $this->expectNotToPerformAssertions();
            (new Testme())->testMe(2);
        }
        

        【讨论】:

        • 解释或 cmets 会有所帮助
        【解决方案8】:

        我偶然发现了同样的问题。为了确保“什么都没有”发生,只需在单元测试中调用该方法就足够了。如果失败了,测试无论如何都会失败。

        如果你只是在没有 @expectedException 注释的情况下调用你的方法

        public function test()
        {
            new Testme(1);
        }
        

        你会得到一个错误

        There was 1 error:
        
        1) Testme::testMe
        Exception: YAY
        

        【讨论】:

        • 那么这如何回答这个问题?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-03-06
        • 1970-01-01
        • 2017-02-27
        • 2017-03-05
        相关资源
        最近更新 更多