Skip to content

feat: Add support for separate cassettes per DataProvider cases #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions .github/workflows/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write

strategy:
fail-fast: false
matrix:
php-versions: ['8.2', '8.3', '8.4']
php-versions: [ '8.2', '8.3', '8.4' ]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4

- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
Expand All @@ -26,7 +28,7 @@ jobs:

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
Expand All @@ -43,4 +45,42 @@ jobs:
run: vendor/bin/phpstan analyse

- name: Execute PHPUnit tests
run: vendor/bin/phpunit
id: phpunit
run: |
if [[ "${{ matrix.php-versions }}" == "8.3" && "${{ github.ref }}" == "refs/heads/main" && "${{ github.event_name }}" == "push" ]]; then
vendor/bin/phpunit --coverage-clover coverage.xml
echo "generate-coverage-badge=true" >> $GITHUB_OUTPUT
else
echo "generate-coverage-badge=false" >> $GITHUB_OUTPUT
vendor/bin/phpunit
fi

- name: Generate coverage badge
uses: timkrase/phpunit-coverage-badge@v1.2.1
if: steps.phpunit.outputs.generate-coverage-badge == 'true'
with:
coverage_badge_path: ./output/coverage.svg
push_badge: false
report: coverage.xml

- name: Check badge file exists
if: steps.phpunit.outputs.generate-coverage-badge == 'true'
id: check_badge
run: |
if [ -f "./output/coverage.svg" ]; then
echo "Coverage badge file found"
echo "badge_exists=true" >> $GITHUB_OUTPUT
else
echo "badge_exists=false" >> $GITHUB_OUTPUT
echo "Coverage badge file not found, skipping push to image-data branch"
fi

- name: Deploy badge to image-data branch
if: steps.phpunit.outputs.generate-coverage-badge == 'true' && steps.check_badge.outputs.badge_exists == 'true'
uses: peaceiris/actions-gh-pages@v4
with:
publish_dir: ./output
publish_branch: image-data
github_token: ${{ secrets.GITHUB_TOKEN }}
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
170 changes: 167 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# PHP-VCR integration for PHPUnit

![Coverage](https://raw.githubusercontent.com/angelov/phpunit-php-vcr/image-data/coverage.svg)

A library that allows you to easily use the PHP-VCR library in your PHPUnit tests.

## Requirements
Expand Down Expand Up @@ -34,7 +36,8 @@ Then, add the extension to your PHPUnit configuration file.
## Usage

The library provides an `UseCassette` attribute that can be declared on test classes or specific test methods. The
attribute expects one string argument - the name of the cassette.
attribute accepts a cassette name and optional parameters for advanced functionality like separate cassettes per
data provider case.

When running the tests, the library will automatically turn the recorder on and off, and insert the cassettes when
needed.
Expand All @@ -54,7 +57,7 @@ responses in the given cassette.
{
#[Test]
public function example(): void { ... }

#[Test]
public function another(): void { ... }
}
Expand Down Expand Up @@ -102,4 +105,165 @@ used for that method. In this example, the responses from the requests made in t
#[UseCassette("example_2.yml")]
public function recorded(): void { ... }
}
```
```

## DataProvider Support

The library supports PHPUnit's `DataProvider` functionality with additional options for managing cassettes when using data providers.

### Basic DataProvider Usage

When using a data provider with the basic `UseCassette` attribute, all test cases from the data provider will share the same cassette file:

```php
use Angelov\PHPUnitPHPVcr\UseCassette;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
#[Test]
#[UseCassette("shared_cassette.yml")]
#[DataProvider("urls")]
public function testWithDataProvider(string $url): void
{
$content = file_get_contents($url);
// All test cases will use the same cassette file
}

public static function urls(): iterable
{
yield ["https://example.com"];
yield ["https://example.org"];
}
}
```

### Separate Cassettes Per DataProvider Case

For more granular control, you can create separate cassette files for each data provider case using the `separateCassettePerCase` parameter:

```php
use Angelov\PHPUnitPHPVcr\UseCassette;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
#[Test]
#[UseCassette(name: "separate_cassettes.yml", separateCassettePerCase: true)]
#[DataProvider("urls")]
public function testWithSeparateCassettes(string $url): void
{
$content = file_get_contents($url);
// Each test case will have its own cassette file:
// - separate_cassettes-0.yml
// - separate_cassettes-1.yml
}

public static function urls(): iterable
{
yield ["https://example.com"];
yield ["https://example.org"];
}
}
```

### Named DataProvider Cases

When using named data provider cases, the cassette files will use the case names:

```php
use Angelov\PHPUnitPHPVcr\UseCassette;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
#[Test]
#[UseCassette(name: "named_cassettes.yml", separateCassettePerCase: true)]
#[DataProvider("namedUrls")]
public function testWithNamedCassettes(string $url): void
{
$content = file_get_contents($url);
// Each test case will have its own cassette file:
// - named_cassettes-example-com.yml
// - named_cassettes-example-org.yml
}

public static function namedUrls(): iterable
{
yield 'example.com' => ["https://example.com"];
yield 'example.org' => ["https://example.org"];
}
}
```

### Grouping Cassettes in Directories

To organize separate cassette files in directories, use the `groupCaseFilesInDirectory` parameter:

```php
use Angelov\PHPUnitPHPVcr\UseCassette;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
#[Test]
#[UseCassette(
name: "organized_cassettes.yml",
separateCassettePerCase: true,
groupCaseFilesInDirectory: true
)]
#[DataProvider("urls")]
public function testWithOrganizedCassettes(string $url): void
{
$content = file_get_contents($url);
// Cassette files will be organized in a directory:
// - organized_cassettes/0.yml
// - organized_cassettes/1.yml
}

public static function urls(): iterable
{
yield ["https://example.com"];
yield ["https://example.org"];
}
}
```

### Class-Level DataProvider Support

The dataProvider functionality also works when the `UseCassette` attribute is declared at the class level:

```php
use Angelov\PHPUnitPHPVcr\UseCassette;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[UseCassette(name: "class_level.yml", separateCassettePerCase: true)]
class ExampleTest extends TestCase
{
#[Test]
#[DataProvider("urls")]
public function testMethod(string $url): void
{
$content = file_get_contents($url);
// Each test case will have separate cassettes:
// - class_level-0.yml
// - class_level-1.yml
}

public static function urls(): iterable
{
yield ["https://example.com"];
yield ["https://example.org"];
}
}
```
43 changes: 23 additions & 20 deletions src/Subscribers/AttributeResolverTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,62 @@
namespace Angelov\PHPUnitPHPVcr\Subscribers;

use Angelov\PHPUnitPHPVcr\UseCassette;
use Angelov\PHPUnitPHPVcr\Values\TestCaseParameters;
use Angelov\PHPUnitPHPVcr\Values\TestMethodInfo;
use Exception;
use ReflectionMethod;

trait AttributeResolverTrait
{
private function needsRecording(string $test): bool
{
return $this->getAttribute($test) !== null;
return $this->getTestCaseCassetteParameters($test) !== null;
}

private function getCassetteName(string $test): ?string
private function getTestCaseCassetteParameters(string $test): ?TestCaseParameters
{
return $this->getAttribute($test)?->name;
}

private function getAttribute(string $test): ?UseCassette
{
$test = $this->parseMethod($test);
$testMethodDetails = $this->parseMethod($test);

try {
if (PHP_VERSION_ID < 80300) {
$method = new ReflectionMethod($test);
$method = new ReflectionMethod($testMethodDetails->method);
} else {
// @phpstan-ignore-next-line
$method = ReflectionMethod::createFromMethodName($test);
$method = ReflectionMethod::createFromMethodName($testMethodDetails->method);
}
} catch (Exception) {
return null;
}

$attributes = $method->getAttributes(UseCassette::class);

if ($attributes) {
return $attributes[0]->newInstance();
$cassette = $attributes ? $attributes[0]->newInstance() : $this->getAttributeFromClass($testMethodDetails);
if ($cassette === null) {
return null;
}

return $this->getAttributeFromClass($test);
return new TestCaseParameters(
cassette: $cassette,
case: $testMethodDetails->dataProvider,
);
}

private function parseMethod(string $test): string
private function parseMethod(string $test): TestMethodInfo
{
$test = explode(" ", $test)[0];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@angelov
I didn't get the idea of this explode. Why did you use it?
I removed this line because it doesn't allow us to have space in dataprovider cases.
for instance if my case is

yield 'My use case name' => [...]

It will result in dataProvider name 'my`


return explode("#", $test)[0];
$methoDetails = explode("#", $test);
return new TestMethodInfo(
method: $methoDetails[0],
dataProvider: $methoDetails[1] ?? null
);
}

private function getAttributeFromClass(string $test): ?UseCassette
private function getAttributeFromClass(TestMethodInfo $test): ?UseCassette
{
if (PHP_VERSION_ID < 80300) {
$method = new ReflectionMethod($test);
$method = new ReflectionMethod($test->method);
} else {
// @phpstan-ignore-next-line
$method = ReflectionMethod::createFromMethodName($test);
$method = ReflectionMethod::createFromMethodName($test->method);
}
$class = $method->getDeclaringClass();
$attributes = $class->getAttributes(UseCassette::class);
Expand Down
30 changes: 28 additions & 2 deletions src/Subscribers/StartRecording.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Angelov\PHPUnitPHPVcr\Subscribers;

use Angelov\PHPUnitPHPVcr\UseCassette;
use Angelov\PHPUnitPHPVcr\Values\TestCaseParameters;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;
use VCR\VCR;
Expand All @@ -20,10 +22,34 @@ public function notify(Prepared $event): void
return;
}

$cassetteName = $this->getCassetteName($test);
assert($cassetteName !== null);
$testCaseCassetteParameters = $this->getTestCaseCassetteParameters($test);
assert($testCaseCassetteParameters instanceof TestCaseParameters);

if ($testCaseCassetteParameters->case !== null) {
$cassetteName = $this->makeCassetteNameForCase(
case: $testCaseCassetteParameters->case,
cassette: $testCaseCassetteParameters->cassette,
);
} else {
$cassetteName = $testCaseCassetteParameters->cassette->name;
}

VCR::turnOn();
VCR::insertCassette($cassetteName);
}

private function makeCassetteNameForCase(string $case, UseCassette $cassette): string
{
if (!$cassette->separateCassettePerCase) {
return $cassette->name;
}
$cassetteNameParts = explode('.', $cassette->name);
$cassetteSuffix = $cassette->groupCaseFilesInDirectory ? '/' . $case : '-' . $case;
if (count($cassetteNameParts) === 1) {
//cassette name does not contain a dot, so we can use it as is
return $cassette->name . $cassetteSuffix;
}
$ext = array_pop($cassetteNameParts);
return implode('.', $cassetteNameParts) . $cassetteSuffix . '.' . $ext;
}
}
Loading