Skip to content

Commit 1211cca

Browse files
authored
Add support for separate cassettes per DataProvider cases (#10)
1 parent a1c7bca commit 1211cca

29 files changed

+920
-28
lines changed

README.md

Lines changed: 165 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ Then, add the extension to your PHPUnit configuration file.
3434
## Usage
3535

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

3940
When running the tests, the library will automatically turn the recorder on and off, and insert the cassettes when
4041
needed.
@@ -54,7 +55,7 @@ responses in the given cassette.
5455
{
5556
#[Test]
5657
public function example(): void { ... }
57-
58+
5859
#[Test]
5960
public function another(): void { ... }
6061
}
@@ -102,4 +103,165 @@ used for that method. In this example, the responses from the requests made in t
102103
#[UseCassette("example_2.yml")]
103104
public function recorded(): void { ... }
104105
}
105-
```
106+
```
107+
108+
## DataProvider Support
109+
110+
The library supports PHPUnit's `DataProvider` functionality with additional options for managing cassettes when using data providers.
111+
112+
### Basic DataProvider Usage
113+
114+
When using a data provider with the basic `UseCassette` attribute, all test cases from the data provider will share the same cassette file:
115+
116+
```php
117+
use Angelov\PHPUnitPHPVcr\UseCassette;
118+
use PHPUnit\Framework\Attributes\DataProvider;
119+
use PHPUnit\Framework\Attributes\Test;
120+
use PHPUnit\Framework\TestCase;
121+
122+
class ExampleTest extends TestCase
123+
{
124+
#[Test]
125+
#[UseCassette("shared_cassette.yml")]
126+
#[DataProvider("urls")]
127+
public function testWithDataProvider(string $url): void
128+
{
129+
$content = file_get_contents($url);
130+
// All test cases will use the same cassette file
131+
}
132+
133+
public static function urls(): iterable
134+
{
135+
yield ["https://example.com"];
136+
yield ["https://example.org"];
137+
}
138+
}
139+
```
140+
141+
### Separate Cassettes Per DataProvider Case
142+
143+
For more granular control, you can create separate cassette files for each data provider case using the `separateCassettePerCase` parameter:
144+
145+
```php
146+
use Angelov\PHPUnitPHPVcr\UseCassette;
147+
use PHPUnit\Framework\Attributes\DataProvider;
148+
use PHPUnit\Framework\Attributes\Test;
149+
use PHPUnit\Framework\TestCase;
150+
151+
class ExampleTest extends TestCase
152+
{
153+
#[Test]
154+
#[UseCassette(name: "separate_cassettes.yml", separateCassettePerCase: true)]
155+
#[DataProvider("urls")]
156+
public function testWithSeparateCassettes(string $url): void
157+
{
158+
$content = file_get_contents($url);
159+
// Each test case will have its own cassette file:
160+
// - separate_cassettes-0.yml
161+
// - separate_cassettes-1.yml
162+
}
163+
164+
public static function urls(): iterable
165+
{
166+
yield ["https://example.com"];
167+
yield ["https://example.org"];
168+
}
169+
}
170+
```
171+
172+
### Named DataProvider Cases
173+
174+
When using named data provider cases, the cassette files will use the case names:
175+
176+
```php
177+
use Angelov\PHPUnitPHPVcr\UseCassette;
178+
use PHPUnit\Framework\Attributes\DataProvider;
179+
use PHPUnit\Framework\Attributes\Test;
180+
use PHPUnit\Framework\TestCase;
181+
182+
class ExampleTest extends TestCase
183+
{
184+
#[Test]
185+
#[UseCassette(name: "named_cassettes.yml", separateCassettePerCase: true)]
186+
#[DataProvider("namedUrls")]
187+
public function testWithNamedCassettes(string $url): void
188+
{
189+
$content = file_get_contents($url);
190+
// Each test case will have its own cassette file:
191+
// - named_cassettes-example-com.yml
192+
// - named_cassettes-example-org.yml
193+
}
194+
195+
public static function namedUrls(): iterable
196+
{
197+
yield 'example.com' => ["https://example.com"];
198+
yield 'example.org' => ["https://example.org"];
199+
}
200+
}
201+
```
202+
203+
### Grouping Cassettes in Directories
204+
205+
To organize separate cassette files in directories, use the `groupCaseFilesInDirectory` parameter:
206+
207+
```php
208+
use Angelov\PHPUnitPHPVcr\UseCassette;
209+
use PHPUnit\Framework\Attributes\DataProvider;
210+
use PHPUnit\Framework\Attributes\Test;
211+
use PHPUnit\Framework\TestCase;
212+
213+
class ExampleTest extends TestCase
214+
{
215+
#[Test]
216+
#[UseCassette(
217+
name: "organized_cassettes.yml",
218+
separateCassettePerCase: true,
219+
groupCaseFilesInDirectory: true
220+
)]
221+
#[DataProvider("urls")]
222+
public function testWithOrganizedCassettes(string $url): void
223+
{
224+
$content = file_get_contents($url);
225+
// Cassette files will be organized in a directory:
226+
// - organized_cassettes/0.yml
227+
// - organized_cassettes/1.yml
228+
}
229+
230+
public static function urls(): iterable
231+
{
232+
yield ["https://example.com"];
233+
yield ["https://example.org"];
234+
}
235+
}
236+
```
237+
238+
### Class-Level DataProvider Support
239+
240+
The dataProvider functionality also works when the `UseCassette` attribute is declared at the class level:
241+
242+
```php
243+
use Angelov\PHPUnitPHPVcr\UseCassette;
244+
use PHPUnit\Framework\Attributes\DataProvider;
245+
use PHPUnit\Framework\Attributes\Test;
246+
use PHPUnit\Framework\TestCase;
247+
248+
#[UseCassette(name: "class_level.yml", separateCassettePerCase: true)]
249+
class ExampleTest extends TestCase
250+
{
251+
#[Test]
252+
#[DataProvider("urls")]
253+
public function testMethod(string $url): void
254+
{
255+
$content = file_get_contents($url);
256+
// Each test case will have separate cassettes:
257+
// - class_level-0.yml
258+
// - class_level-1.yml
259+
}
260+
261+
public static function urls(): iterable
262+
{
263+
yield ["https://example.com"];
264+
yield ["https://example.org"];
265+
}
266+
}
267+
```

src/Subscribers/AttributeResolverTrait.php

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,65 @@
55
namespace Angelov\PHPUnitPHPVcr\Subscribers;
66

77
use Angelov\PHPUnitPHPVcr\UseCassette;
8+
use Angelov\PHPUnitPHPVcr\Values\TestCaseParameters;
9+
use Angelov\PHPUnitPHPVcr\Values\TestMethodInfo;
810
use Exception;
911
use ReflectionMethod;
1012

1113
trait AttributeResolverTrait
1214
{
1315
private function needsRecording(string $test): bool
1416
{
15-
return $this->getAttribute($test) !== null;
17+
return $this->getTestCaseCassetteParameters($test) !== null;
1618
}
1719

18-
private function getCassetteName(string $test): ?string
20+
private function getTestCaseCassetteParameters(string $test): ?TestCaseParameters
1921
{
20-
return $this->getAttribute($test)?->name;
21-
}
22-
23-
private function getAttribute(string $test): ?UseCassette
24-
{
25-
$test = $this->parseMethod($test);
22+
$testMethodDetails = $this->parseMethod($test);
2623

2724
try {
2825
if (PHP_VERSION_ID < 80300) {
29-
$method = new ReflectionMethod($test);
26+
$method = new ReflectionMethod($testMethodDetails->method);
3027
} else {
3128
// @phpstan-ignore-next-line
32-
$method = ReflectionMethod::createFromMethodName($test);
29+
$method = ReflectionMethod::createFromMethodName($testMethodDetails->method);
3330
}
3431
} catch (Exception) {
3532
return null;
3633
}
3734

38-
$attributes = $method->getAttributes(UseCassette::class);
35+
$cassetteAttribute = $method->getAttributes(UseCassette::class);
3936

40-
if ($attributes) {
41-
return $attributes[0]->newInstance();
37+
$cassetteAttributeInstance = $cassetteAttribute
38+
? $cassetteAttribute[0]->newInstance() : $this->getAttributeFromClass($testMethodDetails);
39+
40+
if ($cassetteAttributeInstance === null) {
41+
return null;
4242
}
4343

44-
return $this->getAttributeFromClass($test);
44+
return new TestCaseParameters(
45+
cassetteInfo: $cassetteAttributeInstance,
46+
case: $testMethodDetails->dataProvider,
47+
);
4548
}
4649

47-
private function parseMethod(string $test): string
50+
private function parseMethod(string $test): TestMethodInfo
4851
{
49-
$test = explode(" ", $test)[0];
52+
$methodDetails = explode("#", $test);
5053

51-
return explode("#", $test)[0];
54+
return new TestMethodInfo(
55+
method: $methodDetails[0],
56+
dataProvider: $methodDetails[1] ?? null
57+
);
5258
}
5359

54-
private function getAttributeFromClass(string $test): ?UseCassette
60+
private function getAttributeFromClass(TestMethodInfo $test): ?UseCassette
5561
{
5662
if (PHP_VERSION_ID < 80300) {
57-
$method = new ReflectionMethod($test);
63+
$method = new ReflectionMethod($test->method);
5864
} else {
5965
// @phpstan-ignore-next-line
60-
$method = ReflectionMethod::createFromMethodName($test);
66+
$method = ReflectionMethod::createFromMethodName($test->method);
6167
}
6268
$class = $method->getDeclaringClass();
6369
$attributes = $class->getAttributes(UseCassette::class);

src/Subscribers/StartRecording.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Angelov\PHPUnitPHPVcr\Subscribers;
66

7+
use Angelov\PHPUnitPHPVcr\UseCassette;
8+
use Angelov\PHPUnitPHPVcr\Values\TestCaseParameters;
79
use PHPUnit\Event\Test\Prepared;
810
use PHPUnit\Event\Test\PreparedSubscriber;
911
use VCR\VCR;
@@ -20,10 +22,38 @@ public function notify(Prepared $event): void
2022
return;
2123
}
2224

23-
$cassetteName = $this->getCassetteName($test);
24-
assert($cassetteName !== null);
25+
$testCaseCassetteParameters = $this->getTestCaseCassetteParameters($test);
26+
assert($testCaseCassetteParameters instanceof TestCaseParameters);
27+
28+
if ($testCaseCassetteParameters->case !== null) {
29+
$cassetteName = $this->makeCassetteNameForCase(
30+
case: $testCaseCassetteParameters->case,
31+
cassetteInfo: $testCaseCassetteParameters->cassetteInfo,
32+
);
33+
} else {
34+
$cassetteName = $testCaseCassetteParameters->cassetteInfo->name;
35+
}
2536

2637
VCR::turnOn();
2738
VCR::insertCassette($cassetteName);
2839
}
40+
41+
private function makeCassetteNameForCase(string $case, UseCassette $cassetteInfo): string
42+
{
43+
if (!$cassetteInfo->separateCassettePerCase) {
44+
return $cassetteInfo->name;
45+
}
46+
47+
$cassetteNameParts = explode('.', $cassetteInfo->name);
48+
$cassetteSuffix = $cassetteInfo->groupCaseFilesInDirectory ? '/' . $case : '-' . $case;
49+
50+
if (count($cassetteNameParts) === 1) {
51+
//the cassette name does not contain a dot, so we can use it as is
52+
return $cassetteInfo->name . $cassetteSuffix;
53+
}
54+
55+
$ext = array_pop($cassetteNameParts);
56+
57+
return implode('.', $cassetteNameParts) . $cassetteSuffix . '.' . $ext;
58+
}
2959
}

src/UseCassette.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
use Attribute;
88

99
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
10-
class UseCassette
10+
readonly class UseCassette
1111
{
12-
public function __construct(public readonly string $name)
13-
{
12+
public function __construct(
13+
public string $name,
14+
public bool $separateCassettePerCase = false,
15+
public bool $groupCaseFilesInDirectory = false
16+
) {
1417
}
1518
}

src/Values/TestCaseParameters.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Angelov\PHPUnitPHPVcr\Values;
6+
7+
use Angelov\PHPUnitPHPVcr\UseCassette;
8+
9+
readonly class TestCaseParameters
10+
{
11+
public function __construct(
12+
public UseCassette $cassetteInfo,
13+
public ?string $case = null,
14+
) {
15+
}
16+
}

src/Values/TestMethodInfo.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Angelov\PHPUnitPHPVcr\Values;
6+
7+
use InvalidArgumentException;
8+
9+
readonly class TestMethodInfo
10+
{
11+
public ?string $dataProvider;
12+
13+
public function __construct(
14+
public string $method,
15+
?string $dataProvider = null
16+
) {
17+
$this->dataProvider = $this->normaliseDataProvider($dataProvider);
18+
}
19+
20+
private function normaliseDataProvider(?string $dataProvider): ?string
21+
{
22+
if ($dataProvider === null) {
23+
return null;
24+
}
25+
26+
$replaced = (string)preg_replace('/-+/', '-', (string)preg_replace('/\W+/', '-', $dataProvider));
27+
28+
if ($replaced === '') {
29+
throw new InvalidArgumentException('Invalid data provider name: ' . $dataProvider);
30+
}
31+
32+
return trim(strtolower($replaced));
33+
}
34+
}

0 commit comments

Comments
 (0)