Donnie Ashok
Donnie Ashok

Reputation: 1405

PHPUnit How to test against different variable every time?

I am writing a Product Class, the job of the Class is to take in a product id and to output the corresponding product name.

For e.g.:

$Product = new Product;
$Product->id = "ff62";
$Product->readId();
echo $Product->name;

// returns a string with at least 5 characters.

My PHPUnit test method looks like:

$Product = new Product;
$Product->id = "ff62"; // needs to be a variable
$Product->readId();
$this->assertEquals(gettype($Product->name), 'string');

However, my aim is to check for a different product ID each time instead of ff62 which may or may not exist in database.

Ideally one should be able to define the id variable during testing.

What is the best way to test for dynamic variables as such?

Upvotes: 1

Views: 1591

Answers (3)

DrDamnit
DrDamnit

Reputation: 4826

Faker is one way to do it, but I would hesitate to say it is the "best way."

Your requirements are: 1. Test a set of different variables. 2. Those variables may or may not exist in the database.

But you have several problems with how you have designed this test:

  1. You are using gettype() and comparing it to string. This is a bad idea. If product 54 is "foo", and your test is returning "bar" for 54, it will pass. This is Programming by Coincidence. I.e., it works, but not on purpose.
  2. The way you're setting this up does not really deal with the problem. While Faker can create fake data, it cannot automatically create known good and known bad data for your specific system and business cases. I would assume that you want to test known good data + expected results as well as known bad data + expected exceptions.

The proper way to structure this test is using @dataProvider and database fixtures / testing.

Here's what that would look like:

<?php
namespace Foo\Bar;

use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
use \PDO;
USE \Exception;

class ProductTest extends TestCase
{
    use TestCaseTrait;


    // only instantiate pdo once for test clean-up/fixture load
    static private $pdo = null;

    // only instantiate PHPUnit_Extensions_Database_DB_IDatabaseConnection once per test
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
        }

        return $this->conn;
    }

    public function getDataSet()
    {
        return $this->createMySQLXMLDataSet('tests/unit/testdata/sampleproductdata.xml');
    }

    /**
     * Tests products against known good data in the database fixture.
     * @param $id
     * @param $expectedName
     * @dataProvider providerTestProduct
     */

    public function testProduct($id, $expectedName) {
        $Product = new Product;
        $Product->id = $id;
        $Product->readId();
        $this->assertSame($expectedName, $Product->name);
    }

    /**
     * Provides data that should appear in the database.
     * @return array
     */
    public function providerTestProduct() {
                // id , expectedName
        return  [ [ "ff62" , "fooproduct"]
                , [ "dd83" , "barproduct"]
                , [ "ls98" , "bazproduct"]
                ];
    }

    /**
     * Tests products against known-bad data to ensure proper exceptions are thrown.
     * @param $id
     * @param $expectedName
     */
    public function testProductExceptions($id, $expectedName) {
        $Product = new Product;
        $Product->id = $id;

        $this->expectException(Exception::class);
        $Product->readId();
    }

    /**
     * Provides test data that when queried against the database should produce an error.
     * @return array
     */
    public function providerTestProductExceptions() {
        // id , expectedName
        return  [ [ "badtype" , "fooproduct"]  //Wrong id type
                , [ "aaaa" , "barproduct"]     //Does not exist
                , [ null   , "bazproduct"]     //null is a no-no.
        ];
    }
}

Here's a breakdown:

  1. Use namespaces. Because it's 2018, and it's the right thing to do.
  2. Use use to declare what classes you're using in the test.
  3. Use TestCaseTrait to properly setup your TestCase
  4. The private $pdo variable will hold your database connection for your class / test.
  5. getConnection() is required. This will use the database, username, and password you have configured in your phpunit.xml file. Reference
  6. getDataSet() goes and reads your datasource (fixture), then, truncates your database on your workstation / dev box, imports all the data from the fixture to put the database in a known state. (Be sure to backup your data before you do this. It's lossy on purpose. Never execute on production).

Next, you have two pairs of methods for the test cases: a test and a data provider.

The data provider in each case provides an ID you want to test, and the expected result. In the case of testProduct and providerTestProduct, we are providing ID that should exist in the database (as ensured by the fixture above). We can then check that Product::readId() is not only returning a string, but is actually returning the correct string.

In the second case, testProductException() and providerTestProductException(), we are intentionally sending bad values to the class to trigger exceptions, and then checking to make sure those bad values actually produces the desired behavior: failure / thrown exceptions.

Upvotes: 2

Donnie Ashok
Donnie Ashok

Reputation: 1405

I found out the best way to do this is to use Faker.

https://github.com/fzaninotto/Faker

While I was trying to test against different instances of a Product, I could definitely use Faker to randomly generate a product and test if the Product was being retrieved properly from the database.

Although majorly used in Laravel, Symfony, etc. It's quite easy to use even in custom PHP frameworks.

Upvotes: 0

Kishlin
Kishlin

Reputation: 373

You can randomise your dataset using random number generation.

$value = dechex(random_int(0, 255)).dechex(random_int(0, 255));

$Product = new Product;
$Product->id = $value;
$Product->readId();
$this->assertEquals('string', gettype($Product->name));
$this->assertEquals($value, $Product->name);

One usually puts the expected value to the left, and the actual one to the right.

Upvotes: 0

Related Questions