Skip to main content

PHPUnit

All PHPUnit tests start with a subclass of TestCase. You can then add one or more test case methods to that class, each of which must be public and start with test. In Codewars' PHP versions 7.4+, PHPUnit requires the name of the test class to end with Test. For the same versions, every PHP file (user code, tests, and preloaded if anything is in there) should have the opening tag <?php, and every test file should have the import use PHPUnit\Framework\TestCase;. The PHP runner will concatenate them if missing, but it is a legacy behavior that should not be relied on for new content.

Basic Setup

Solution

<?php

function add(int $a, int $b): int {
    return $a + $b;
}
<?php

function add(int $a, int $b): int {
    return $a + $b;
}

Tests

<?php

use PHPUnit\Framework\TestCase;

class AddTest extends TestCase {
    public function testAdd() {
        $message = "for input a = 1, b = 2";
        $expected = 3;
        $actual = add(1, 2);
        $this->assertSame($expected, $actual, $message);
    }
}
<?php

use PHPUnit\Framework\TestCase;

class AddTest extends TestCase {
    public function testAdd() {
        $message = "for input a = 1, b = 2";
        $expected = 3;
        $actual = add(1, 2);
        $this->assertSame($expected, $actual, $message);
    }
}

Assertions

Argument order

PHPUnit's assertions follow the parameter order ($expected, $actual), unlike most testing libraries which have $actual first.

Primitive equality

assertEquals has surprising behavior on primitives due to casting and loose equality. Here are sample comparisons that assertEquals considers equal:

$this->assertEquals("1", "001");
$this->assertEquals(1, "001");
$this->assertEquals(42, true);
$this->assertEquals("42", true);
$this->assertEquals(0, false);
$this->assertEquals([["foo" => 1]], [["foo" => "1"]]);
$this->assertEquals("1", "001");
$this->assertEquals(1, "001");
$this->assertEquals(42, true);
$this->assertEquals("42", true);
$this->assertEquals(0, false);
$this->assertEquals([["foo" => 1]], [["foo" => "1"]]);

assertSame fails all of the above checks, as one would likely want. assertEquals should be avoided due to its surprising behavior.

Object equality

It's tempting to use assertSame across the board, but its identity check is too strict for most object comparisons:

$expected = new stdClass;
$expected->foo = "foo";
$expected->bar = "42";
$actual = new stdClass;
$actual->foo = "foo";
$actual->bar = "42";
$this->assertSame($expected, $actual); // fails
$expected = new stdClass;
$expected->foo = "foo";
$expected->bar = "42";
$actual = new stdClass;
$actual->foo = "foo";
$actual->bar = "42";
$this->assertSame($expected, $actual); // fails

This is because === / assertSame between PHP objects will merely check if they point to the same object (reference equality). == / assertEquals between objects should still be avoided, because its semantics are very surprising, and it compares object properties with loose comparison.

Here, using assertObjectsEqual, which calls a class' equals(self $other): bool method, might be a more appropriate approach.

Float equality

For float comparisons, assertEqualsWithDelta has similar unpredictable behavior as assertEquals:

$this->assertEqualsWithDelta(0.99, "1", 0.1); // passes
$this->assertEqualsWithDelta(99999, true, 0.1); // passes
$this->assertEqualsWithDelta(0.99, "1", 0.1); // passes
$this->assertEqualsWithDelta(99999, true, 0.1); // passes

Type-checking the arguments to assertEqualsWithDelta before calling it (with assertIsFloat) or implementing a custom delta assertion based on assertTrue may be a safer bet.

Another suprising fact is that unlike most languages, PHP considers 1 and 1.0 not to be strictly equal, so assertSame(1, 1.0) will fail. If you want to allow them to be strictly equal, you might want to cast the user's answer, e.g. if (is_int($actual)) $actual = (float)$actual, or the other way around.