Unit Testing with PHPUnit

7 questions found

What is PHPUnit, and what is a unit test meant to actually verify about a piece of code?

Beginner
PHPUnit is the most widely used testing framework in the PHP ecosystem, providing the tools needed to write, organize, and run automated tests against your code, and a unit test specifically focuses on verifying that one small, isolated piece of code, typically a single function or method, behaves exactly as expected when given a specific set of inputs, checking that it produces the correct output or correctly performs the expected action, without needing to manually and repeatedly test that same logic by hand every single time a change is made.
public function testAddReturnsCorrectSum(): void
{
    $calculator = new Calculator();
    $this->assertEquals(5, $calculator->add(2, 3));
}
Real-world example A developer writes a unit test verifying that a Calculator class's add method correctly returns five when given the inputs two and three, and can now run that same test automatically at any time to instantly confirm that behavior still works correctly.

Common follow-ups: What is the difference between a unit test and an integration test?;How do you actually run PHPUnit tests from the command line?

Static Analysis (PHPStan & Psalm);Functions & Scope

What are assertions in PHPUnit, and how do commonly used assertions like assertEquals and assertTrue let you verify expected behavior within a test?

Beginner
An assertion is a statement within a test that checks whether an actual value matches an expected value, and if that check fails, the entire test is immediately marked as failed with a clear message describing exactly what was expected versus what was actually received, and PHPUnit provides a large number of built in assertion methods covering many common comparison needs, including assertEquals to check that two values are equal, assertTrue and assertFalse to check a boolean condition, and assertCount to check the exact number of items within an array.
public function testUserIsActiveByDefault(): void
{
    $user = new User('Sarah');
    $this->assertTrue($user->isActive());
}
Real-world example A test verifying that a newly created User object is active by default uses the assertTrue assertion to directly check the boolean result returned by calling the isActive method on that new object.

Common follow-ups: What happens when an assertion within a test actually fails?;What other useful assertion methods does PHPUnit provide beyond the most commonly used ones?

Type Declarations & Strict Types;OOP

What is the arrange act assert pattern, and how does structuring a test around these three distinct steps make tests clearer and easier to understand?

Intermediate
The arrange act assert pattern is a widely recommended way to structure the internal logic of an individual test, first arranging any necessary setup, such as creating the objects and data the test will need, then performing the single specific action being tested, such as calling the particular method under test, and finally asserting that the actual resulting outcome matches what was genuinely expected, and consistently following this three step structure across every test makes each individual test easier to read and understand at a glance, even for someone unfamiliar with that specific piece of code.
public function testDiscountIsAppliedCorrectly(): void
{
    // Arrange
    $cart = new ShoppingCart();
    $cart->addItem('Book', 20.00);

    // Act
    $cart->applyDiscount(0.10);

    // Assert
    $this->assertEquals(18.00, $cart->getTotal());
}
Real-world example A test verifying a shopping cart's discount logic clearly separates the setup of adding an item, the actual action of applying a ten percent discount, and the final assertion checking the resulting total, making the test's intent immediately obvious to anyone reading it.

Common follow-ups: Does every single test always need to strictly follow this exact three step structure?;How does this pattern relate to keeping individual tests focused on testing just one specific behavior?

Functions & Scope;Design Patterns in PHP

What are mock objects in PHPUnit, and why are they useful when testing a piece of code that depends on an external service, such as sending an email or calling a third party API?

Intermediate
A mock object is a fake, controlled substitute for a real dependency, created specifically for use within a test, and using a mock instead of the actual real dependency lets you test a piece of code in complete isolation without needing to genuinely send a real email, make an actual external API call, or hit a real database, while also letting you precisely control exactly what that fake dependency returns and verify exactly how many times and with what specific arguments it was actually called, which makes tests significantly faster, more reliable, and fully repeatable regardless of the availability of any real external service.
public function testOrderConfirmationEmailIsSent(): void
{
    $mailer = $this->createMock(MailerInterface::class);
    $mailer->expects($this->once())->method('send');

    $orderService = new OrderService($mailer);
    $orderService->confirmOrder($order);
}
Real-world example A test verifying that placing an order triggers a confirmation email uses a mock mailer object instead of an actual real mailer, confirming the send method was called exactly once without ever genuinely sending a real email during the test run.

Common follow-ups: What is the difference between a mock, a stub, and a fake in testing terminology?;How do you configure a mock object to return a specific, predetermined value when one of its methods is called?

Email Sending in PHP (PHPMailer & SMTP);Dependency Injection & Service Containers

What is test coverage, and why should achieving a high test coverage percentage not necessarily be treated as the sole ultimate goal of a testing strategy?

Intermediate
Test coverage measures what percentage of a codebase's actual lines or branches of code are executed at least once while running the full test suite, and while reasonably high test coverage is generally a positive sign, it should not be treated as the sole measure of genuine test quality, since a test can technically execute a line of code without actually verifying that the code produced the correct result, meaning a codebase could show high coverage while still containing weak, ineffective tests that would fail to actually catch a real bug if one were introduced.
vendor/bin/phpunit --coverage-html coverage-report
Real-world example A team notices their codebase shows ninety five percent test coverage but still experiences frequent production bugs, and upon investigation discovers many of their existing tests execute the relevant code without actually asserting anything meaningful about its correctness.

Common follow-ups: What is the difference between line coverage and branch coverage?;What are better indicators of genuine test quality beyond a simple coverage percentage?

Static Analysis (PHPStan & Psalm);Design Patterns in PHP

How do data providers in PHPUnit let you run the exact same test logic repeatedly against many different sets of input data, without duplicating the test method itself?

Advanced
A data provider is a separate method that returns a collection of different input and expected output combinations, and by associating a test method with a data provider, PHPUnit will automatically run that same single test method once for every individual data set the provider returns, which avoids writing many nearly identical, separate test methods that only differ in their specific input values, and makes it very easy to thoroughly test a piece of logic against a wide range of different scenarios, including various edge cases, all through one single, reusable test method.
public function additionProvider(): array
{
    return [[1, 1, 2], [2, 3, 5], [-1, 1, 0]];
}

/** @dataProvider additionProvider */
public function testAdd(int $a, int $b, int $expected): void
{
    $this->assertEquals($expected, (new Calculator())->add($a, $b));
}
Real-world example A test verifying an addition method uses a data provider supplying several different pairs of numbers along with their correctly expected sums, running the exact same underlying test logic automatically against each individual data set without needing to write several nearly duplicate test methods.

Common follow-ups: Can a data provider itself depend on external data, such as reading from a file?;How does PHPUnit report a failure when only one specific data set within a data provider actually fails?

Type Declarations & Strict Types;Functions & Scope

How does test driven development, commonly abbreviated as TDD, change the typical order in which tests and actual implementation code are written, and what benefits does this workflow claim to offer?

Advanced
Test driven development inverts the usual order most developers are accustomed to, by first writing a failing test that clearly describes the exact expected behavior of a piece of functionality that does not yet exist at all, then writing just enough actual implementation code to make that specific failing test pass, and finally refactoring that implementation to improve its internal quality while continuously confirming the test still passes throughout, and proponents argue this disciplined workflow naturally results in more thoroughly tested code, more focused and minimal implementations, and a design that is inherently easier to test from the very beginning, since testability is considered from the very first step rather than added on afterward.
// Step 1: Write a failing test
public function testCalculatesShippingCost(): void
{
    $this->assertEquals(5.99, (new ShippingCalculator())->calculate(2.5));
}

// Step 2: Write minimal code to make it pass
// Step 3: Refactor while keeping the test passing
Real-world example A developer building a new shipping cost calculation feature writes a failing test describing the exact expected cost for a given package weight before writing any actual implementation code at all, then implements just enough logic to make that test pass.

Common follow-ups: Is test driven development suitable for every single type of feature or project?;What is the typical criticism raised against strictly following test driven development for absolutely everything?

Functions & Scope;Design Patterns in PHP