Colocating Tests and Production Code in PHP(Unit)
Tags: php, tutorial, testing, tdd
Published: 2022-11-22 07:43:57

Colocating tests and production code is the default in some programming languages and has become the de facto standard in others. I have to admit that while I liked it in Go and TypeScript, I always felt a little queasy about doing it in PHP. I couldn't really name it; it was more a sense of "it doesn't feel right". Of course, it turned out to be just a bullshit bias, because it works just as well in PHP as it does in other languages. No matter which language, the benefits are the same:

- You see at first glance if there are tests for a piece of code
- You don't have to navigate as much anymore
- You don't have to maintain parallel structures in source and test directories

Let's give it a try, shall we?

## Migrating a simple project

Take this setup with test and production code in parallel structures and a single test suite:

```
src/
└── MyProject
    ├── MyClass.php
    └── MyOtherClass.php
tests/
└── MyProject
    ├── MyClassTest.php
    └── MyOtherClassTest.php
```

```xml
<testsuites>  
    <testsuite name="default">  
        <directory>tests</directory>  
    </testsuite>
</testsuites>
```

The good news is that migration is as easy as dragging and dropping the `MyProject` directory from `tests` to `src`; at least if you're using PhpStorm. PhpStorm will take care of changing namespaces appropriately. If your editor of choice doesn't offer this kind of refactoring, search and replace will do as well.

```
src/
└── MyProject
    ├── MyClass.php
    ├── MyClassTest.php
    ├── MyOtherClass.php
    └── MyOtherClassTest.php
```

The last thing you need to do is point PHPUnit to the new location. Just change the directory from `tests` to `src` and you're ready to go:

```xml
<testsuites>  
    <testsuite name="default">  
        <directory>src</directory>  
    </testsuite>
</testsuites>
```

## What about multiple test suites?

When there is more than a single test suite, things get a bit tricky. Let's take a look at this setup:

```
tests/
├── Integration
│   └── MyProject
│       └── MyClassTest.php
└── Unit
    └── MyProject
        └── MyOtherClassTest.php
```

```xml
<testsuites>  
    <testsuite name="integration">  
        <directory>tests/Integration</directory>  
    </testsuite>
    <testsuite name="unit">  
        <directory>tests/Unit</directory>  
    </testsuite>
</testsuites>
```

If we colocate them the same way we did in the first example, there's no way to determine which test belongs to which test suite. Here are two options to approach this problem.

### Using suffixes

The first option is to change the suffix `Test` to either `IntegrationTest` or `UnitTest`. This allows a clear distinction to which suite a test belongs, although it's a bit clunky.

```
src
└── MyProject
    ├── MyClass.php
    ├── MyClassIntegrationTest.php
    ├── MyOtherClass.php
    └── MyOtherClassUnitTest.php
```

```xml
<testsuites>
    <testsuite name="integration">  
        <directory suffix="IntegrationTest.php">src</directory>  
    </testsuite>
    <testsuite name="unit">  
        <directory suffix="UnitTest.php">src</directory>  
    </testsuite>
</testsuites>
```

### Using groups

The second options is to use the [group](https://phpunit.readthedocs.io/en/9.5/annotations.html#group) annotation to categorize tests. Since groups and test suites are different things and a test suite in PHPUnit cannot be composed of groups, the configuration can be simplified to a single test suite. Just remember that you have to use the `group` option instead of the `testsuite` option when running tests (`phpunit --group integration` or `phpunit --group unit`).

```php
/**
 * ...
 * @group integration
 */
final class MyClassTest extends TestCase
{
	// ...
}


/**
 * ...
 * @group unit
 */
final class MyOtherClassTest extends TestCase
{
	// ...
}
```

```xml
<testsuites>
    <testsuite name="default">  
        <directory>src</directory>  
    </testsuite>
</testsuites>
```

## Tweaking PhpStorm

I was a little disappointed when I discovered that the files in my project window looked like this:

![Snippet of PhpStorm's project window: test and production classes without clear distinction](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dihdmwpc6a2c8z8nimal.png)

The test classes have the red/green arrow indicating a test, but I was hoping they would be highlighted as I was used to with jest tests. Fortunately, this is easy to customize.

Go to **Preferences | Appearance & Behavior | Scopes** and set up a new scope matching test classes (`file:*Test.php`):

![Screenshot of PhpStorm's settings dialog (Preferences | Appearance & Behavior | Scopes)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uxf7mepixacutrk634l1.png)

Then add an element for the new scope and pick a color under **Preferences | Appearance & Behavior | File Colors**:

![Screenshot of PhpStorm's settings dialog (Preferences | Appearance & Behavior | File Colors)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/87xuxiecmr2zo28s5kvq.png)

It should look something like this now:

![Snippet of PhpStorm's project window: test classes highlighted green](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ww3so8a1dgr85j7u1ca.png)

Sweet!

Another thing you may want to tweak is the severity of certain inspections. I noticed warnings showing up that weren't there before. This is probably due to the way PhpStorm handles files based on what type of directory they're in (**Preferences | Directories**). Since we already have our new scope set up, we just have to change the severity under **Preferences | Editor | Inspections** to mute unwanted inspections.

![Screenshot of PhpStorm's settings dialog (Preferences | Editor | Inspections)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/porgk5sf55pc1sc4bxes.png)

## That's it

That's all I have to say on this topic at the moment. I've been using this approach in my personal projects for over a year now, and introduced it at work about a month ago. So far, everyone likes it.