---
title: 'PHP Unit Testing - PHPUnit Tutorial - Full PHP 8 Tutorial'
source: 'https://youtube.com/watch?v=9-X_b_fxmRM'
video_id: '9-X_b_fxmRM'
date: 2026-07-28
duration_sec: 1864
---

# PHP Unit Testing - PHPUnit Tutorial - Full PHP 8 Tutorial

> Source: [PHP Unit Testing - PHPUnit Tutorial - Full PHP 8 Tutorial](https://youtube.com/watch?v=9-X_b_fxmRM)

## Summary

This lesson covers writing unit tests with PHPUnit in PHP. It walks through installing PHPUnit via Composer, configuring it with an XML file, and writing tests for a simple Router class. Key topics include test structure, assertions, data providers, setup methods, and strict vs. loose comparisons.

### Key Points

- **Installing PHPUnit** [0:05] — Install PHPUnit as a dev dependency using Composer: `composer require --dev phpunit/phpunit`. After installation, the phpunit script is located in vendor/bin.
- **Creating PHPUnit Configuration** [2:27] — Create a phpunit.xml file in the project root to set options like colors and bootstrap script. Use a `<testsuites>` element to define test directories.
- **Writing a Basic Test** [4:42] — Create a test class extending PHPUnit\Framework\TestCase. Use the `@test` annotation or prefix method names with 'test' to mark them as tests. Use assertions like `assertEquals` to verify expected outcomes.
- **Test Method Naming** [7:41] — PHPUnit recognizes test methods via `@test` annotation or by prefixing the method name with 'test'. Snake case or camel case can be used; snake case is preferred for readability with long names.
- **Using Data Providers** [9:50] — Data providers allow running the same test with multiple sets of data. Define a public method that returns an array of arrays, and use the `@dataProvider` annotation to link it to the test method.
- **Setup Method** [12:58] — The `setUp()` method is called before each test to initialize common objects, reducing code duplication. It is ideal for creating instances of the class under test.
- **Testing Exceptions** [14:33] — Use `expectException()` to assert that an exception is thrown. This method must be called before the code that triggers the exception.
- **Strict vs. Loose Comparisons** [28:00] — `assertEquals` uses loose comparison (==), which can cause false positives. `assertSame` uses strict comparison (===) and is recommended to avoid type-related bugs.

### Conclusion

PHPUnit is a powerful testing framework for PHP. By writing unit tests, you can catch bugs early, refactor with confidence, and ensure your code behaves as expected. Use strict assertions and data providers to write robust tests.

## Transcript

[Music]
in the last lesson we talked about
different methods and techniques of
testing in this lesson we'll actually
write some unit tests and learn the
basics of php unit which is a testing
framework for php first we need to
install the php unit so let's go to the
php units documentation and follow the
installation steps there within the
installation section we see the option
to install it with composer so let's
click on that and we'll copy the command
and paste it into the terminal so this
is the command that we're going to copy
and notice that we're installing php
unit as a dev dependency which is why
there is dash dash dev option here so
let's take this go to the terminal paste
it in and hit enter after the
installation is complete we can open the
composer.json file to confirm that php
unit dependency was added and we can see
here that it was added as a dev
dependency within the required dev
section we can then run tests using the
php unit script that is located within
the vendor bin directory so if we open
the project files here and open the
vendor directory and within the bin
directory we see this php unit script so
that's what we use to run the tests so
if we type in vendor bin php unit and
hit enter we get bunch of available
options that we can use along with the
command we can run tests for a specific
file or an entire folder so we could
specify a specific test file here for
example my test.php or we could specify
an entire directory and you will run all
the tests within that directory or we
could use the filter option to run the
specific test so as you can see here
there are lots of options at your
disposal and you don't need to worry
about all of them you will get used to
them as you work on phpunit and you will
probably only use few of them also if
you did not want to type out vendor bin
php unit every single time you could
also create an alias for it to be
organized we're going to create a
dedicated directory for tests where
we're going to place our test files in
so we're going to create a new directory
called tests and within the test
directory we're going to create another
directory called unit which will hold
all of our unit tests this way we can
have our unit tests within its own
namespace and directory and later we
could have a directory for integration
or future tests and so on it is up to
you how you want to structure your tests
but this is a pretty common way before
we create the actual test file let's
first configure the php unit by creating
a configuration file php unit
configuration along with the other
benefits helps us set some of the
options that we would otherwise have to
include within the command so let's
create the php unit configuration file
and it's an xml file in xml format so
we're going to create phpunit.xml file
within the project root i'm going to
paste in the basic php unit
configuration snippet here so let's go
over it quick as i mentioned before it's
an xml file so it's an xml format within
the php unit we specify the options in
the form of attributes so we're setting
the colors option to true for color
coding our test results and we're
setting the bootstrap script to the
autoloading script from the vendor
directory bootstrap script is loaded
before the tests are run and it can be
set to a custom bootstrap script if
needed you can check the documentation
for other options that you can configure
for which i'll leave the link in the
description then within the php unit
element we have the test suites element
for better organization and because we
are only writing unit tests for now we
only have a single test suite element
here for the unit tests because we have
the unit directory here that's what this
is referencing too later we could add
another test suite for integration or
feature tests and so on so if i open the
terminal now and run the phpunite script
without specifying the directory it will
run through the test suites that are
defined in the configuration file and as
you can see we're not getting any
options we're getting a warning stating
that no tests were executed which makes
sense because we haven't written any
tests yet we're going to write some
tests for the router class that we
created in the preview section of the
series if you haven't watched it or
don't remember it no worries i'll go
over what the router class does it's a
pretty simple class where we have a few
methods available to register different
routes like a get and post routes for
example they simply call the register
method which adds the route along with
its action to the route's property the
action can either be an array that
contains a class name like a controller
and a method that it needs to run or it
can simply be a callable then we have
the routes method that returns the
registered routes and we have the
resolve method that resolves the route
based on the request eri and the request
method so it's a very basic router
nothing fancy but just because it's
simple doesn't mean it should have no
tests so let's add a new unit test class
called router test and notice the naming
convention here we're following the same
regular class naming convention with the
test appended at the end let's click ok
and we're going to extend phpunit's test
case class which provides the
functionality for the tests the actual
tests are written in the form of methods
within the test class i'm going to open
the router class in a split view so that
it's easier to follow and see what we're
testing the first thing that we might
want to test is to confirm that a route
can be registered so we're targeting the
register method to test if we scroll up
we're targeting this method basically we
can call the test method something like
it registers a route so we'll do public
function it registers a route and you
can name tests any way you want it can
be named something like route can be
registered or test that route is
registered and so on i personally prefer
descriptive names even if they are long
so it is up to you how you want to name
your tests when i write tests i usually
follow the given when then or arrange
act assert patterns sometimes it also
helps to write out what it is that you
are actually testing in words or
sentences for example let's write out
what we are actually testing in
sentences here so given that we have a
router object when we take action or
when we call a register method and
provide the arguments then we assert the
expected outcome or in this case we
assert that route was registered so
let's fill this in now given that we
have a router object so will the router
equals new router when we call the
register method and provide the
arguments so we'll do a router register
and in this case it does not really
matter what arguments we provide because
we are not testing the resolving part we
are testing that our route is registered
so we'll provide some route here we'll
do the get method and the route will be
users and the action will be some users
maybe controller or class and the method
will be index and for the final part
where we need to assert we can use one
of the assertion methods from the php
units test case class so if we type this
assert we can see bunch of available
assertions here we can use assert equals
to compare the expected value with the
actual value in this case we'll simply
do expected array and we'll fill this in
in a second and the actual value we can
get from the routes method from the
router object so we'll do routes because
as you remember the routes method right
here simply returns the routes property
so now we just need to create this
expected variable so let's create it
right here and if we look at the
structure of the routes property if we
scroll up to the register method we can
see what the expected value should be so
let's build up the expected value here
manually so we'll do get as the key and
it contains a single route in this case
and the key of that route is the users
and the value is users index let's run
the test and see if it passes and we're
getting the warning that no tests were
found within the router test class the
reason for that is because phpunit does
not know if this method is a test method
or just a regular method we can hint it
using annotation or we could prefix the
method name with the word test let's add
the annotation first so i'm going to add
the annotation test here and run it
again and sure enough he detected one
test and one assertion and our test
passed let's try with the test prefix so
we can simply prefix this method with
the test so we can say test that it
registers a route and we can get rid of
the annotation and let's run the test
again and sure enough it still works
it's up to you which one you want to use
either way is fine as far as the naming
conventions go it's also up to you and
your team what conventions you want to
follow if you strictly follow psr then
you would probably use camel case for
method names so in that case this would
turn into a camelcase method name and
there is nothing wrong with that a lot
of developers along with some frameworks
follow that convention but for me it's a
bit harder to read when method names are
long using camel case which is why i use
snake case for test methods i'm going to
revert that back to the annotation and
let's continue writing some tests we can
add a test to confirm that get and post
routes are registered properly when we
call the get and post methods now you
could add two unit tests one to test the
get method and the other one to test the
post method or you could add a single
unit test to test both some developers
prefer to combine them into a single
test some prefer to have it a separate
test it is up to you how you want to do
it but i personally like to have
separate tests so for example in this
case we're going to test that a get
route is registered so we'll do public
function it registers a get route and
we'll add another test
where it registers a post route we'll
add the annotation to both
and we'll pretty much copy this entire
thing here we're going to remove the
comments and instead of register we'll
call the get method and remove this
first argument and everything else
should be the same so we'll again copy
this and paste it here we'll replace the
get with post and we'll replace the get
method with the post method we can also
replace the index with store to be more
appropriate and everything else remains
the same so as i mentioned some
developers might prefer to test them
together so you could do something like
combine them and register both routes
and simply build up the expected array
properly and test it with a single
assertion i personally prefer to have
the separate tests because i want to
test the get method in isolation and i
want to test the post method in
isolation as well so let's get rid of
this here and let's run the test to see
if it passes so i'm going to run the
tests here and now we have three tests
and three assertions and everything is
passing if we were refactoring the post
method here and changed it to put for
example but kept the method name as post
our test would be able to catch that so
if we run the test again we see that now
it failed because this test where we're
expecting to register a post route the
expected value doesn't match the actual
value so then we would go in here and
fix it up and run the tests again and
now it's passing let's also add a unit
test for the routes method to confirm
that no routes are registered initially
when we first create the router object
so we'll do something like public
function there are no routes when router
is created so given that we have the
router object we simply need to assert
that the routes returns an empty array
we can use assert empty assertion for
that so we'll do this assert empty
router route let's run the test and in
this case i'm simply going to filter it
so that it runs the test for this method
only so we'll do dash dash filter and
specify the test method and looks like
our test failed according to the error
it seems that we're trying to access the
property before it has been initialized
if we look at the routes method we see
that it simply returns the routes
property right and if we scroll up here
where the routes property is defined we
see that it's not being initialized and
we don't have a constructor where we're
initializing the routes property we're
not registering any routes so the error
message makes perfect sense and a simple
unit test like this helped us find a bug
in the code this can easily be fixed by
initializing the property to an empty
array either directly in the property
definition or within the constructor in
this case we'll initialize it within the
property definition to an empty array
and let's run the test again and sure
enough now it passes sometimes we want
to prepare some objects or some data
before tests are run for example in all
of these tests here we're creating the
new instance of the router object the
same way so we're doing a router equals
new router and we're repeating that over
and over in all of our tests now this
usually isn't the problem when we have
the simple objects like this but it
becomes tricky when you are passing
along some dependencies to the
instantiated objects or you require some
more setup of the object that is then
used in most of your tests for example
if we were passing some dependencies
here some other objects into the router
constructor or maybe we were calling
some more methods here to set up the
router object and we needed that for all
of our tests then we would be kind of
duplicating that block of code over and
over again we could actually share that
setup code across our tests by using a
method called setup so we'll define a
setup method here and this method is
called before each test is run we're
going to create the router object here
and assign it to the router property so
we'll do this router equals to new
router
and then we can simply replace router
with this router everywhere else so
we'll do router
this router and replace all and we'll
also add the router property and then we
can scroll down and get rid of the extra
lines so we'll drop this because we no
longer need it we'll also get rid of
these comments since we no longer need
them we'll get rid of this scroll down
get rid of that and technically we could
also get rid of this but i'm going to
leave that in because this unit test
specifically tests the scenario where
we're creating a new router and we're
confirming that it has no register
routes so yes this would still work if
we get rid of this this would still work
but in this case i would prefer to
instantiate a new router object just in
case we were doing something with the
router object in the setup method so
we'll do router and router or if you
want to make it in a single line we'll
drop that and do it this way let's clear
this out and run the tests to make sure
that everything is still working and
sure enough everything passes we have
four tests and four assertions we could
also define a method called tear down
which is called at the end of each test
even if the test fails tear down can be
used to clean up heavy objects or some
external resources that were created in
the setup method in this case we don't
need teardown method because we're
working with a simple object so we're
not going to create it alright let's
move on to the result method which has a
bit more logic to it the first test that
makes sense is to test that a route not
found exception is prone if we scroll
down to the result method we see that
route not found exception can be thrown
for multiple reasons one is when the
action is not set which means that it
could either be the wrong request method
or the wrong route and other reasons
could be that a class doesn't exist or
method doesn't exist and so on also just
a quick note here this check here is
array check is technically not needed
because we are essentially checking if
the action is either callable or array
and it cannot be anything other than
callable or array because we have type
hinted right here right here and right
here so technically action will not be
anything other than callable or array so
we could drop this because if it was
callable it would execute this block and
because it's not callable then it's
assumed that it's array so we could
technically drop that but before we drop
this i'm going to write the test so that
we can refactor it and be sure that our
tests still pass now we could write a
separate unit test for each of these
scenarios where the route not found
exception is thrown or we could simply
use something called data provider and
provide different cases to the unit test
and phpunit will run the test for all
the given cases so let's create a unit
test here called something like eat
throws route not found exception and
let's register a couple of routes here
so we're going to register a post route
for the user's endpoint which will
execute the store method on some kind of
users class and then we'll also register
the get route for the users and this
will simply be index now this is the
given right so then what we need to do
is that we need to call the resolve
method so when we try to resolve a route
we want to assert that route not found
exception is thrown so we're going to do
this router resolve and we'll pass in
some kind of request uri and some kind
of request method now i'm not hard
coding this because if i were to hard
code this then you would only be testing
a single scenario right a single test
case but we want to use the data
provider where these two arguments here
are given by the data provider before we
do that though we need to assert that an
exception was thrown to assert that an
exception is wrong we can use expect
exception method so we could type this
expect exception and as you can see we
can also assert exception message code
and so on but we only care for the
exception class for now so we'll do
expect exception route not found
exception and the expect exception
assertion has to happen before calling
the method that actually throws the
exception otherwise if we did it the
other way and we put expect exception
after this would throw the exception and
it would stop the execution so we need
to add this beforehand so now we have
our test set up properly we are
asserting that exception is thrown the
only other thing left is to provide the
data provider here where we provide the
request eri and request methods a test
method can accept x number of arguments
and it can be provided by data provider
method so we could say that this test
method expects request dri and request
method now this of course will not work
because we haven't specified the data
provider yet we can add the data
provider by using the annotation as well
so we'll do data provider followed by
the method name whose return will be
passed as arguments to the test method
so we'll call this route not found cases
and as you can see the ide is
highlighting it here because this method
doesn't exist so we need to create this
data provider method based on the
definition from the documentation this
method needs to be public and must
return an array of arrays or it must
return an object that implements the
iterator interface which yields an array
for each iteration so let's create this
method here we'll do public function
route not found cases and we'll return
an array now this needs to return an
array of arrays so basically it's an
array that contains arrays as values and
these array values are the basically
each individual cases and whatever you
provide here are going to be passed as
arguments to the test method that uses
that data provider so if we say here foo
bar who is going to be passed in as
request eri and bar is going to be
passed in as request method so let's
write out some failing cases that would
result in route not found exception so
the first two cases are when request
method is not found and the route is not
found we have the post and get routes
for the index so the first one is easy
we'll do users
and put now in this case the route is
found but the request method is not
found the other case would be invoices
and post where the request method is
found but the route is not found the
next case would be when the class does
not exist now providing a failing case
for this scenario is actually pretty
simple because the user's class doesn't
exist right this is just a string it's
not really a class so we could provide
the proper route and a proper method but
it will still fail because the class
does not exist so we'll do users and get
and this is going to get us up to here
but it's going to fail because class
does not exist and it will throw the
route not found exception and for the
last case where the method doesn't exist
for that we need to somehow simulate a
way where the class exists and i don't
want to hard code some controller here
or something like that it wouldn't make
sense because we're doing unit testing
and we don't want to depend on other
classes like controllers for example so
what we can do is that we can use
anonymous classes to kind of simulate a
controller so we'll define some kind of
class here like users and set it to
anonymous class then within the
anonymous class we'll define some kind
of method let's say we'll have a method
to delete an user so we'll do a delete
and this returns true and now instead of
using users here we could simply replace
these users with users scope resolution
operator class so now when we make a
post request to the user's endpoint it
should execute the store method on the
user's class and the user's class exists
because we're simulating it as an
anonymous class but the store method
does not exist so essentially we should
get here and this should fail and you
should still throw this exception so
we'll add that failing case here so
we'll do users and post let's run the
test to make sure that it works i'm
going to filter it again to only run
this test so i'm going to copy that and
filter and sure enough as you can see
four tests were found and four
assertions now these four tests are not
the same four tests as the previous test
result that we have this was four tests
entirely for the router test class but
these four tests are just the four test
cases here so each individual case here
is its own test if you run the test
without filtering you'll see that it
should say eight instead of four and
sure enough we have eight tests and
eight assertions now we can be confident
that if we refactor this code and
something breaks our tests should be
able to catch such issues so we run the
tests everything is still fine and if we
keep refactoring and we make some kind
of mistake where we only throw the
exception if the class doesn't exist
then this will fail because it is not
throwing an exception when the method
doesn't exist so as you can see we are
getting a failing test saying that data
set number three and the data set start
from zero so zero one two three it's
this one and these are the values of the
arguments and the message says that it
failed asserting that exception was
thrown so we'll revert that back run it
again and it passes so as you can see
the data providers can be used to
simplify tests and test against multiple
cases you can even reference to external
data providers say for example you had
this data provider method extracted in a
data provider class you could use fully
qualified class name to reference this
method that way multiple test classes
could use the same data provider for
example we could have a directory like
data providers here so we'll do data
providers and we could add a new class
here called something like router data
provider and simply extract this method
to here and then we can reference it by
using fully qualified class names so we
need to add the fully qualified class
name here so we need to do tests data
providers router data provider scope
resolution operator route not found
cases and now as you can see within the
id it's even clickable and i can click
into it and this should still work so if
i run the tests again we see that our
test fails so let's figure out why our
test is failing the error says that the
router data provider class does not
exist but we have created the class and
we have the proper namespaces but it
still says that it doesn't exist so
what's the issue let's open our
composer.json file and within our
composer.json file we see that we are
using the auto loading for the app name
space and the app directory we need to
do the same thing for the test directory
because we have the test directory out
of the app directory so it's not using
the same auto loading as this so we need
to add another autoloading section here
so we'll duplicate this and we'll do
auto load dev because we only need it
for the development and we'll change
this to tests and tests now we'll run
the composer dump auto load
let's run the tests again and everything
is back to normal data providers are
cool but don't try to use data providers
for everything basically don't overuse
it try to look at your code and see if
the data provider makes sense it's
useful for things like validations where
you want to provide valid data and
assert that everything passes or you
want to provide invalid data and assert
that it fails kind of like what we did
here where we're providing the invalid
cases and asserting that an exception is
thrown for all those cases now of course
this router class itself is not perfect
it is not meant to be used in production
this is just a simple router example
that we wrote in the previous section
and i decided to write tests for it
because it was better than writing tests
for some kind of full bar examples also
in a real application you would have
more complicated router because you may
want to resolve other possible methods
like put and delete and maybe accept the
arguments within the route that he would
then pass to the controllers and so on
also in this case where we're throwing
the route not found exception instead of
throwing route not found exception here
we could be throwing different type of
exceptions and then we could have
different tests for it to test
individual cases for example in here if
the class doesn't exist we could throw
class not found exception if the method
doesn't exist we could throw method not
found exception and both of them could
extend from the route not found
exception and so on let's write couple
more tests for the result method so far
we've written tests where we're testing
the failures right we're testing this
and we're testing essentially this but
we haven't tested this section and we
haven't tested this section when it
actually passes so the first test we're
going to write is to assert that it
resolves properly when the action is
callable so we'll do something like this
we'll do public function it resolves
route from a closure we'll register a
route here so we'll do this router get
and we'll use slash users and we'll pass
in the closure as the action instead of
an array and we'll simply let it return
one two three to indicate that it's
returning some type of user ids or
something it doesn't matter what we're
actually returning in this case just
that we're returning something that we
could then assert now this is the given
so when we actually call the resolve
method and pass in the correct request
uri and request method we want to assert
that this is what gets resolved to so
this is what needs to be returned from
the resolve method so we'll do this
assert equals array of one two and three
and the actual result will be this
router resolve request eri is users and
the request method is get let's clean
this up a little bit so it's easier to
read and let's test this out we're going
to open the terminal and let's run the
tests and sure enough now we have nine
tests and nine assertions which means
that this test also passed if we
commented this out for example and run
the tests we see that this test will now
fail so let's revert that and let's
write the last test for today which
should test that a route is actually
resolved properly so we could again use
the anonymous class for that so let's
write the test first we'll do public
function it resolves
route and we're going to use the
anonymous class here as well so we're
going to copy the anonymous class from
the previous test and paste it here and
we'll just change the method to
something like index and we can do the
return of array and we can actually
return the same thing one two and three
though as i mentioned before the return
doesn't really matter for this example
because we're not testing controllers
we're testing the router class and
ensuring that a route gets resolved
properly then we're going to register a
get route for the users and we're going
to use the users as our class and the
index as our method and finally we'll
assert that an array of 1 2 and 3 equals
to the actual resolved value and that
would be this router resolve request eri
is users and method is get let's run the
tests and make sure that it's passing
and sure enough we have 10 tests and 10
assertions which means that all of our
tests in the router test are passing
before we wrap up for today i want to
talk about the assert equals method
because assert equals does not do strict
comparison and it can sometimes result
in false positives which can give you an
impression that you have no bugs because
your tests are passing but you actually
have some bugs for example if we changed
one of the elements from the returned
array here to something like string one
when we use assert equals this will
still pass so if we open the terminal
and run the tests everything is still
passing but technically should not pass
because this and this are not the same
this is a string one and this is an
integer one in this specific example it
does not really matter because it's not
the bug in our code we're creating this
anonymous class within the test and
we're asserting it here so it's not a
big deal but if this was a method on a
class within the app that was expected
to return zero for example but instead
it returned boolean false using assert
equals would make test pass so it could
result in false positives so for example
if we change this to boolean and we
returned false and simply think about it
that this is an actual class within our
app class and not an anonymous class so
think about it that it's a controller
maybe or something like that and we're
asserting that the return value of the
resolve is zero now this is going to
pass if we run vendor being phpunit we
see that everything is passing so you're
getting the impression that everything
is fine with your code but something
somewhere might blow up because it's a
false positive and it's better to use
strict comparisons the way you can do
that is that there is another assertion
method called assert same and assert
same in addition to comparing the value
it also compares the type so if i run
this again we see that this test fails
and it says that it failed asserting
that false is identical to zero and the
key word here is identical so
essentially a third same is pretty much
the same as identity operator so you
could think of it as assert equals as
double equal sign comparison or the
comparison operator and assert same is
same as the triple equal comparison or
the identity operator i personally use
assert same in most cases and would
recommend it as well just be aware that
when comparing two objects a certain
equals will pass when comparing two
different objects with the same property
values while assert same will fail since
assert same would only pass if two
variables were pointing to the same
object and if you're not sure what i'm
talking about you can watch one of the
previous lessons where i talked about
comparing objects using comparison
operator versus identity operator and
the link for that video is in the
description so what i'm going to do now
is i'm going to revert this back and i'm
going to replace assert equals with a
third same everywhere so we'll do a
third equals replace with a search same
and replace all and let's make sure that
everything still passes everywhere we're
going to run the test again and
everything is still passing so this is
it for this video thank you so much for
watching in the next video we're going
to continue with the php unit and cover
things like test doubles stubs and
mocking if you enjoy my tutorials please
give my videos thumbs up share and
subscribe and i'll see you in the next
one
