I have a simple method get_db_password() which can return a string or under some circumstances it could return null, either case should be considered a valid response.
What I am really testing for is that the script doesnt blow up if the getter is called.
How can I write a test/assertion - which either calls get_db_password() and asserts that the script didnt die, or that can test whether the response was either null or a string. e.g something like
$this->assertInternalType( "string || null", $this->config->get_db_password() );
Source code
<?php
class Config {
/** #var string $db_password stored the database password */
private $db_password;
public function __construct() {
$this->db_password = require(PROJ_DIR . "config/productions.php");
}
/** #return string
*/
public function get_db_password() {
return $this->db_password;
}
}
test code
<?php
class ConfigTest extends PHPUnit\Framework\TestCase {
public $config;
public function test_get_db_password_returns_a_string_or_null() {
$this->config = new Config;
// how can I write this test?
$this->assertInternalType('string || null', $this->config->get_db_password());
}
}
I found this as a satisfactory solution
$this->assertTrue(is_string($pass) || $pass === null);
Related
I started to wonder about what exactly is the purpose of service providers in Laravel, and why they work in the way they do. After searching through some articles,
the key points of service providers in my understanding are:
Simplifies object creation (Laravel What is the use of service providers for laravel)
Decoupling your code (r/laravel: When to use service providers?)
Dependency injection
Reduces technical debt
So it basically binds an implementation to an interface, and we can use it by
$app(MyInterface::class)
or something like that, and we can just change the implementation when needed, only in one place, and the rest of our code which depends on it won't break.
But i still can not grasp the concept, why they are the way they are, it seems overcomplicated. I peaked in to the code, it was certainly a ton of work to make Service Providers & Containers work, so there must be a good reason.
So to learn further, i tried to make my own, more simple version of it, which achieves the same goals. (i obviously lack a lot of info on this, and most probably missed some other goals)
My question is, why would this implementation would not satisfy the same use cases?
Service.php
namespace MyVendor;
/**
* Abstract class for creating services
*/
abstract class Service
{
/**
* Holds the instance of the provided service
*
* #var mixed
*/
private static mixed $instance = null;
/**
* Retrieves the instance of the provided service & creates it on-demand
*
* #return mixed
*/
public static function get(): mixed
{
if (self::$instance === null) {
self::$instance = static::instantiate();
}
return self::$instance;
}
/**
* A function which contains the service's object creation logic
*
* #return mixed
*/
abstract protected static function instantiate(): mixed;
}
Example implementation:
For the example, i chose an interface to parse environment variables, as i already had phpdotenv in my project as a dependency
Services/DotenvParser/DotenvParserInterface.php
namespace MyVendor\Services\DotenvParser;
/**
* This is the service interface i want to provide
*/
interface DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array;
}
Now i will have 2 implementations of this class. I will pretend that a lot of my code already depends on DotenvParserInterface. An old, hacky one which "depends" on another thing, and the replacement for it which uses phpdotenv
A quick fake dependency:
Services/DotenvParser/Dependency.php
namespace MyVendor\Services\DotenvParser;
class Dependency
{
private bool $squeeze;
public string $bar;
public function __construct(string $foo, bool $squeeze)
{
$this->squeeze = $squeeze;
$this->bar = $foo;
if($this->squeeze){
$this->bar .= " JUICE";
}
}
}
Our old code:
Services/DotenvParser/OldDotenvCode.php
namespace MyVendor\Services\DotenvParser;
use BadMethodCallException;
use InvalidArgumentException;
class OldDotenvCode implements DotenvParserInterface
{
/**
* Our fake dependency
*
* #var Dependency
*/
private Dependency $foo;
private string $dir;
private string $fileName;
private string $contents;
private array $result;
public function __construct(Dependency $myDependency)
{
$this->foo = $myDependency;
}
/**
* Implementation of DotenvParserInterface
*
* #param string $directory
* #param string $fileName
* #return array
*/
public function parse(string $directory, string $fileName = ".env"): array
{
try{
$this->setDir($directory)->setFileName($fileName);
}catch(BadMethodCallException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$this->getEnvContents();
$this->contents = $this->getEnvContents();
$this->result = [];
foreach(explode("\n", $this->contents) as $line){
$exploded = explode("=", $line);
$key = $exploded[0];
$value = (isset($exploded[1])) ? trim($exploded[1], "\r") : "";
if($this->foo->bar === "ORANGE JUICE"){
$value = trim($value, "\"");
}
$this->result[$key] = $value;
}
return $this->result;
}
#region Old, bad stuff
public function setDir(string $directory): self{
if(!\is_dir($directory)){
throw new InvalidArgumentException("Directory $directory is not a valid directory");
}
$this->dir = rtrim($directory, "/");
return $this;
}
public function setFileName(string $fileName): self{
if(empty($this->dir)){
throw new BadMethodCallException("Must call method setDir() first with a valid directory path");
}
$fileName = ltrim($fileName, "/");
if(!\file_exists($this->dir . "/" . $fileName)){
throw new InvalidArgumentException("File $fileName does not exist in provided directory {$this->dir}");
}
$this->fileName = $fileName;
return $this;
}
private function getFilePath(): string{
if(empty($this->fileName)){
throw new BadMethodCallException("Must call method setFileName() first");
}
return $this->dir . "/" . $this->fileName;
}
private function getEnvContents(): string{
return \file_get_contents($this->getFilePath());
}
public function setup(): void
{
$this->setDir($directory)->setFileName($fileName);
}
#endregion
}
Now, the phpdotenv version
Services/DotenvParser/phpdotenv.php
namespace MyVendor\Services\DotenvParser;
use Dotenv\Dotenv;
use InvalidArgumentException;
use Dotenv\Dotenv;
use InvalidArgumentException;
class phpdotenv implements DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array
{
try{
Dotenv::createMutable($directory, $fileName)->load();
}catch(\Dotenv\Exception\InvalidPathException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$result = $_ENV;
$_ENV = []; //Hehe
return $result;
}
}
Our service which we made from extending our Service class
Services/DotenvParser/DotenvParserService.php
namespace MyVendor\Services\DotenvParser;
use MyVendor\Service;
class DotenvParserService extends Service
{
// We can do this to make type hinting for ourselves
public static function get(): DotenvParserInterface
{
return parent::get();
}
protected static function instantiate(): DotenvParserInterface
{
$year = 2022;
// Some condition, to return one or another
if($year < 2022){
$dep = new \MyVendor\Services\DotenvParser\Dependency("ORANGE", true);
return new OldDotenvCode($dep);
}
return new phpdotenv();
}
}
And now, we can use it like this:
$dotenvparser = \MyVendor\Services\DotenvParser\DotenvParserService::get();
$result = $dotenvparser->parse(__DIR__);
var_dump($result);
// Outputs an array of our environment variables, yey!
We can also write tests for our services to see if anything breaks:
namespace MyVendorTest\Services\DotenvParser;
use InvalidArgumentException;
use MyVendor\Services\DotenvParser\DotenvParserInterface;
use MyVendor\Services\DotenvParser\DotenvParserService;
final class DotenvParserServiceTest extends \PHPUnit\Framework\TestCase
{
public function doesInstantiate(): void
{
$testParser = DotenvParserService::get();
$this->assertInstanceOf(DotenvParserInterface::class, $testParser);
}
public function testWorksFromValidDirNFile(): void
{
// The actual contents of a .env file
$testArray = [
"DEV_MODE" => "TRUE",
"BASE_HREF" => "http://localhost:8080/"
];
$testParser = DotenvParserService::get();
// phpdotenv loads every parent .env too and i was having none of it for this quick demonstration
$result = $testParser->parse(__DIR__."/../../../", ".env");
$this->assertEquals($testArray, $result);
}
public function testSetupFromInvalidDir(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse("i_am_a_dir_which_does_not_exist");
}
public function testSetupFromInvalidFile(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse(__DIR__, ".notenv");
}
}
So this ended up quite lenghty, but after having that Service class, you basically only need: An interface, at least one implementation of that interface, and a service class which instantiates an implementation of that interface, and optionally some tests for it. And, you can even do dependency injection with it (??) (circular dependencies would get us stuck in an endless loop), like this:
protected static function instantiate(): FooInterface
{
//BarService & AcmeService are extending our Service class
return new FooInterface(BarService::get(), AcmeService::get(), "ORANGE JUICE")
}
I am ready to absorb massive amounts of information
What other things Laravel's Service providers & containers do than i am aware of?
Why and how is it better than a simpler version, like this one?
Does my version really achieve at least those 4 key points i mentioned in the start?
I'm having a problem. I have a unit test that is trying to access methods from a class. My unit test is as follows
public function testReverseArraySuccess()
{
$data = "This is a test.";
$output = (new Reverse)
->setInput($data)
->get();
$this->assertEquals(['test', 'a', 'is', 'This'], $output);
}
My class is a follows
class Reverse
{
public $input;
/**
*
* #param $data
*/
public function setInput($data) {
$this->input = $data;
}
/**
*
*#return void
*/
public function get()
{
return $this->input;
}
}
How can I setup my class so that my Test will pass?
The error I get when I run the test is.
Error : Call to a member function get() on null
setInput isn't chainable since it's not returning an instance of itself. It's a void function that returns null by default that's why you're getting that error message. Update setInput to return an instance of the class to make it chainable.
public function setInput($data) {
$this->input = $data;
return $this;
}
Now get will return whatever value is passed to the input property, here's a working demo.
Then it's a matter of reversing the input, but I'll leave that up OP. 👍
You need to call get data directly after set data by instance of Reverse class.
public function testReverseArraySuccess() {
$data = "This is a test.";
$output = new Reverse;
$output->setInput($data);
$this->assertEquals(['test', 'a', 'is', 'This'], $output->get());
}
I have a unit test class in which I want to instantiate a object from another class in order to that I used setUpBeforeClass() fixtures of phpunit. So if I will use that recently instantiated object directly in test function then its working fine.
If i'll use this object into another function which had been created for data providers. So that object sets to null cause providers always execute first.
Is there a way to call dataProviders just before the test runs, instead?
require_once('Dashboard.php');
Class Someclass extends PHPUnit_Framework_TestCase {
protected static $_dashboard;
public static function setUpBeforeClass()
{
self::$_dashboard = new Dashboard();
self::$_dashboard->set_class_type('Member');
}
/**
* Test Org Thumb Image Existense
* param org profile image : array
* #dataProvider getOrgProfileImages
*/
public function testFieldValidation($a,$b){
//If I call that object function here it will give the result.
//$members = self::$_dashboard->get_members();
//var_dump($members); Printing result as expected
$this->assertTrue(true);
}
public function getOrgProfileImages() : array {
//var_dump(self::$_dashboard);
$members = self::$_dashboard->get_members();
$tmp_array = ['2','2'];
return $tmp_array;
}
public static function tearDownAfterClass()
{
self::$_dashboard = null;
}
}
Error:
The data provider specified for Someclass::testFieldValidation is invalid.
Call to a member function get_members() on null
Please help to mitigate this issue.
Note: since I don't have the source of your Dashboard class, I'm using a random number in the examples below instead
Providers are invoked before any tests are run (and before any hooks, including beforeClass have a chance to run). By far the easiest way to achieve what you're after is to populate that static property on the class load:
use PHPUnit\Framework\TestCase;
/** #runTestsInSeparateProcesses enabled */
class SomeTest extends TestCase
{
public static $_rand = null;
public function provider()
{
$rand = self::$_rand;
var_dump(__METHOD__, getmypid(), 'provided rand', $rand);
return ['rand' => [$rand]];
}
/** #dataProvider provider */
public function testSomething($rand)
{
$this->expectNotToPerformAssertions();
var_dump(__METHOD__, getmypid(), 'tested with', $rand);
}
/** #dataProvider provider */
public function testSomethingElse($rand)
{
$this->expectNotToPerformAssertions();
var_dump(__METHOD__, getmypid(), 'tested with', $rand);
}
}
// this runs before anything happens to the test case class
// even before providers are invoked
SomeTest::$_rand = rand();
Or you could instantiate you dashboard in the provider itself, on the first call:
public function provider()
{
// Instantiate once
if (null === self::$_rand) {
self::$_rand = rand();
}
$rand = self::$_rand;
var_dump(__METHOD__, getmypid(), 'provided rand', $rand);
return ['rand' => [$rand]];
}
#dirk-scholten is right. You SHOULD be creating a new object for each test. It's a GOOD testing practice. Frankly it looks more like you are testing the data and not testing the code, which is fine I guess, it's just not the typical use of PHPUnit. Based on the assumption that you want to make sure every user in the database has a thumbnail image (just guessing), I would go with the following:
<?php
class DashboardDataTest extends PHPUnit\Framework\TestCase {
private $dashboard;
public function setUp() {
$this->dashboard = new Dashboard();
}
/**
* Test Org Thumb Image Existence
* param org profile image : array
*
* #dataProvider getOrgProfileImages
*
* #param int $user_id
*/
public function testThumbnailImageExists(int $user_id){
$thumbnail = $this->dashboard->get_member_thumbnail($user_id);
$this->assertNotNull($thumbnail);
}
public function geOrgUserIDs() : array {
$dashboard = new Dashboard();
// Something that is slow
$user_ids = $dashboard->get_all_the_member_user_ids();
$data = [];
foreach($user_ids as $user_id){
$data[] = [$user_id];
}
return $data;
}
}
Each data provider will get called once and only once before the tests. You do not need a static data fixture on the class because phpunit handles the data fixture for you when you use data providers.
I'm not sure if I can do that, and if I should do that too. I'm writing some tests that could have the same data provider (IP addresses or integers).
class LocalIpAddressTest extends \PHPUnit_Framework_TestCase
{
protected $parser = null;
protected function setUp()
{
$this->parser = new ApacheLogParser();
$this->parser->setFormat('%A');
}
protected function tearDown()
{
$this->parser = null;
}
/**
* #dataProvider successProvider
*/
public function testSuccess($line)
{
$entry = $this->parser->parse($line);
$this->assertEquals($line, $entry->localIp);
}
/**
* #expectedException \Kassner\ApacheLogParser\FormatException
* #dataProvider invalidProvider
*/
public function testInvalid($line)
{
$this->parser->parse($line);
}
public function successProvider()
{
return array(
array('192.168.1.1'),
array('192.168.001.01'),
array('172.16.0.1'),
array('192.168.0.255'),
array('8.8.8.8'),
// not sure about those 2. They are valid ip-format, but can't be assigned as server address
array('0.0.0.0'),
array('255.255.255.255'),
);
}
public function invalidProvider()
{
return array(
// over 255
array('192.168.1.256'),
array('256.256.256.256'),
array('321.432.543.654'),
// incomplete
array('192.168.1.'),
array('192.168.1'),
array('192.168.'),
array('192.168'),
array('192.'),
array('192'),
array(''),
// malformed
array('1921.68.1.1'),
array('192.681.1.'),
array('.1921.68.1.1'),
array('....'),
array('1.9.2.'),
array('192.168.1.1/24'),
// letters (it' not supporting IPv6 yet...)
array('abc'),
array('192.168.1.x'),
array('insert-ip-address-here'),
array('a.b.c.d'),
);
}
}
Then, I have to test when I use $this->parser->setFormat('%a'), that also receives IP Address as an argument. In this case, I'm duplicating all the code just to change one single line. Is it supposed to be that way? Have some way to reuse these Data Providers?
I think you should be able to do that without any problems as long as the dataprovider method is part of the same class. You could include it in a abstract testcase from which your testcase inherits or make use of traits as of php 5.4+.
How do you test multiple values for same Attribute ?
class Test {
private $_optionalValue = null;
function setValue(String $optionalValue)
{
$this->_optionalValue = $optionalValue;
}
}
So here, "$_optionalValue" could be NULL or User defined value, but when I check with phpunit like this :
$optionalValue = PHPUnit_Util_Class::getObjectAttribute($my_object, '_optionalValue');
$this->assertThat(
$optionalValue,
$this->logicalXor(
$this->assertNull($optionalValue),
$this->logicalAnd(
$this->assertAttributeInternalType('string', '_optionalValue', $optionalValue),
$this->assertRegExp('/[0-9]{2}:[0-9]{2}:[0-9]{2}/', (string) $optionalValue)
)
)
);
The Regexp assertion fails because $optionalValue isn't a String (it's null by default)
You are making assertions inside your call to assertThat, but you need to build and pass in constraints instead. All of the methods that start with assert evaluate the value immediately and throw an exception on mismatch. Each assertion has a corresponding constraint class, some with a factory method.
$optionalValue = PHPUnit_Util_Class::getObjectAttribute($my_object, '_optionalValue');
$this->assertThat(
$optionalValue,
$this->logicalXor(
$this->isNull(),
$this->logicalAnd(
new PHPUnit_Framework_Constraint_IsType('string'),
new PHPUnit_Framework_Constraint_PCREMatch('/[0-9]{2}:[0-9]{2}:[0-9]{2}/')
)
)
);
BTW, I do agree that a) you are better off not testing internal state like this and b) you should design your tests so that you know which value to expect. Each test should put the system into a single expected state.. Even code that uses random numbers should substitute a fixed sequence using a stub. Any test that allows multiple possibilities is suspect.
You're testing a private property of an object which should be generally avoided because that's an internal of the unit and you should not care about that.
If your unit needs to validate values of that class, it's likely that generally a value of some kind should be validated.
You could therefore encapsulate the logic of the validation into a unit of it's own, e.g. a validator:
class FooValueValidator implements Validator {
/**
* #var string
*/
private $value;
/**
* #var string
*/
private $regex = '/[0-9]{2}:[0-9]{2}:[0-9]{2}/';
public function __construct($value) {
$this->value = $value;
}
/**
* #return bool
*/
public function isValid() {
if (is_null($this->value)) {
return TRUE;
}
if (!is_string($this->value)) {
return FALSE;
}
$result = preg_match($this->pattern, $this->value);
if (FALSE === $result) {
throw new Exception(sprintf('Regular expression failed.'));
}
return (bool) $result;
}
}
You can then write unit-tests for the validator. You then know that your validator works and you can use it everywhere you like.
class Test {
private $_optionalValue = null;
/**
* #var Validator
*/
private $_validator;
public function __construct(Validator $validator) {
$this->_validator = $validator;
}
function setValue(String $optionalValue)
{
if (!$this->validator->isValid($optionalValue)) {
throw new InvalidArgumentException(sprintf('Invalid value "%s".', $optionalValue));
}
$this->_optionalValue = $optionalValue;
}
}