Phpunit,如何测试一个方法是否什么也没有做?

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

13
请不要默默无言地投反对票!我认为这是一个完全有效的问题,如果您不同意,请解释一下。 @OP:这是一个相关的问题:https://dev59.com/cV4c5IYBdhLWcg3wzs-Q - Quasdunk
1
如果我的原始方法是为了“执行”某些操作而不是“返回”某些内容,那么即使可以返回值,我也不想更改原始代码。 - John Smith
2
不确定你的意思是什么... 相关问题中的解决方案是在测试中包装函数调用到 try-catch 块中。如果抛出异常,就捕获它并手动失败测试($this->fail()),否则只需进行一个虚假断言来通过测试($this->assertTrue(true))。你不需要测试是否返回了任何内容,只需专注于异常。 - Quasdunk
1
只是为了明确起见:目前还没有内置的解决方案。有一个GitHub问题线程讨论了详细信息:https://github.com/sebastianbergmann/phpunit-documentation/issues/171 - Quasdunk
8个回答

27

场景

函数不执行任何操作有两种可能的场景:

场景1:没有返回语句

如果您在函数中不执行任何操作,并且没有包含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 中的一个注释:

如果省略了return语句,那么将返回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 并在参数输入和返回类型中都添加了类型,所以如果创建的对象不是 Email,则 getSut() 函数将首先失败。

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

如何测试执行操作的构造函数

代码异味。构造函数可能不应该执行工作。如果有任何工作,只存储参数。如果构造函数“执行工作”而不仅仅是存储参数,请考虑在getter上进行延迟处理或在工厂中委托该工作。

如何测试“仅存储参数”的构造函数

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

  1. 在第一个测试中测试创建的实例是否属于某个类。
  2. 然后,在另一个不同的测试中,调用一个getter来获取您在构造函数中输入的内容,即使构造函数没有做任何事情(除了存储它)。

希望这可以帮助到您。


2
确实它可以工作,但请注意,如果该方法使用了 PHP 7.1 的 "void" 返回类型,它仍然可以工作,但会被许多静态分析工具报告为 "void method result used"。 - Pierre-Yves
很好的回答,但是:“构造函数的目的是什么?创建某种类型的类,对吗?”实际上,构造函数实例化了某个特定类/类型的对象。 - alexg
是的,您的微调是正确的。构造函数不会“创建一个类”,而是“创建某个类的实例”。没错。我将编辑答案以纳入您的贡献。感谢您的贡献! - Xavi Montero
PHPUnit 7.1版本开始,这种复杂的方法是不再需要的。请参见我下面的答案。 - Tomas Votruba

20
在PHPUnit 7.2+中,您还可以使用TestCase :: expectNotToPerformAssertions()
public function test()
{
    // ...

    $this->expectNotToPerformAssertions();
}

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


这是正确的做法。我尝试写代码时没有添加任何断言,但PHPUnit将该测试标记为不确定的风险。 - RedDragonWebDesign

11

2018+

现在的最佳做法是注释这些情况:

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

3
那里的文档并没有说明这个注解是用于“测试一个方法不执行任何操作”,而是为了避免警告信息,这总是一种不良实践。字面上它说:“@doesNotPerformAssertions 防止一个不执行任何断言的测试被认为是有风险的。” 这个问题并没有问“我如何执行代码而不测试任何东西”,而是“我如何测试它什么也不做”,这是不同的。将报告变成“静音”应该只是临时使用,仅在我们有一些半编码的东西需要禁用冗长输出时使用,而不是作为最终行为。 - Xavi Montero
1
感谢联系。你的评论很难让我理解。 如果我们只看问题中的输入代码(不对其进行重构),@doesNotPerformAssertions 正是测试它的方法。 - Tomas Votruba
1
在我的回答中,我在“如何测试一个什么也不做的构造函数”部分阐述了问题中源代码的情况。new操作符确实会“做”一些事情:创建给定类的新实例。话虽如此,测试代码是$this->assertInstanceOf( HappyClass::class, new HappyClass( $whatever ); - Xavi Montero
1
问题比较通用:“Phpunit,如何测试方法是否“什么也不做”?”虽然你的答案在技术上是正确的,但来自谷歌的人正在寻找他们自己代码的答案。答案应该不仅帮助提问者的确切详细代码,还应该帮助他的所有追随者。“@doesNotPerformAssertions”在这里非常有用。 - Tomas Votruba

3
不可能的。添加return语句并断言结果。
class Testme()
{
    public function testMe ($a)
    {
        if ($a == 1)
        {
            throw new Exception ('YAY');
        }

        return true;
    }
}

并且,然后。
$object = new Testme();
$this->assertTrue($object->testMe(2));

3
注意:此解决方案的功劳归功于这个相关答案。上下文可能看起来有些不同,但解决方案/解决方法的工作方式是相同的。测试异常是否未被抛出与测试没有返回值的方法是一样的。
根据这个问题线程的说法,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);
}

这可能看起来有些粗糙,不是很直观,但如果它看起来很蠢但却能用,那就不算蠢。

2
public function testThrowingException()
{
    $this->expectException(Exception::class);
    $this->expectExceptionMessage('YAY');
    (new Testme())->testMe(1);
}

public function testNotThrowingException()
{
    $this->expectNotToPerformAssertions();
    (new Testme())->testMe(2);
}

仅返回翻译后的文本。不需要解释或注释。 - Alexander Kucheryuk

1
这是一个非常有趣的问题,尽管有很多答案被写出来,但似乎没有一个能够正确回答这个问题,因为您使用了类,让我这样解释一下。
请记住,在类中创建的实例方法应该只有两个意图。
1. 它可以改变类的状态(更改类属性,如私有变量) 2. 它返回类的状态(获取器)
除此之外的任何事情都是无意义的,除非它是静态方法。例如,如果您有像这样的类:
class Foo {

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

方法fooMethod()不返回任何内容,但它会影响类中$prop属性的状态,您可以通过测试该方法来验证。

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

因为您知道如果运行此方法,则应该会影响$prop属性并更改该变量的状态。
杂项场景:我的方法不会更改状态,也不会返回任何状态变量。
那么这个方法是静态的。它不应该是一个实例方法,并且静态方法通常具有返回类型,因为它们不能影响类的状态,也无法返回状态变量。这限制了静态方法存储结果的位置(除非你将它们存储在全局变量中,请勿这样做),因此它应该肯定返回一些输出。如果您不想返回输出,则可以考虑从静态方法返回布尔值。

-1

我也遇到了同样的问题。为了确保“没有什么”发生,只需在单元测试中调用方法即可。如果它失败了,测试也会失败。

如果您只是调用方法而没有使用@expectedException注释,就像这样:

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

你会收到一个错误

There was 1 error:

1) Testme::testMe
Exception: YAY

1
那么这个回答如何解决问题呢? - emfi

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