This document describes the testing strategy and setup for the Digistore24 API Client.
- Overview
- Test Types
- Running Tests
- Code Coverage
- Mutation Testing
- Integration Tests
- Writing Tests
- Continuous Integration
The project uses PHPUnit 11.x for testing with a comprehensive test suite covering:
- Unit Tests: 1035+ tests testing individual components
- Integration Tests: Tests with mocked HTTP responses
- Code Coverage: Tracking test coverage metrics
- Mutation Testing: Infection PHP for test quality validation
- Static Analysis: PHPStan Level 9 for type safety
- Tests: 1035 tests
- Assertions: 2116 assertions
- Code Coverage: ~98% (lines)
- Mutation Score: 85%+ MSI (Mutation Score Indicator)
- PHPStan Level: 9 (maximum)
Test individual classes and methods in isolation:
<?php
namespace GoSuccess\Digistore24\Api\Tests\Unit\Request;
use GoSuccess\Digistore24\Api\Request\BuyUrl\CreateBuyUrlRequest;
use PHPUnit\Framework\TestCase;
class CreateBuyUrlRequestTest extends TestCase
{
public function testCanSetProductId(): void
{
$request = new CreateBuyUrlRequest();
$request->productId = 12345;
$this->assertSame(12345, $request->productId);
}
}Test complete workflows with mocked HTTP responses:
<?php
namespace GoSuccess\Digistore24\Api\Tests\Integration;
use GoSuccess\Digistore24\Api\Digistore24;
use GoSuccess\Digistore24\Api\Client\Configuration;
use PHPUnit\Framework\TestCase;
class BuyUrlWorkflowTest extends TestCase
{
public function testCreateAndListBuyUrls(): void
{
$config = new Configuration(apiKey: 'test-key');
$client = new Digistore24($config);
// Mock HTTP client responses
// Test complete workflow
}
}# Run all tests (fast, no coverage)
composer test
# Run all tests with coverage (requires Xdebug/PCOV)
composer test:coverage# Run only unit tests
composer test:unit
# Run only integration tests
composer test:integration
# Run specific test file
vendor/bin/phpunit tests/Unit/Client/ConfigurationTest.php
# Run specific test method
vendor/bin/phpunit --filter testCanCreateConfiguration# Run with verbose output
vendor/bin/phpunit --testdox
# Stop on first failure
vendor/bin/phpunit --stop-on-failure
# Run tests in random order
vendor/bin/phpunit --order-by=randomTo generate code coverage reports, you need either Xdebug or PCOV installed.
Windows (Laragon/XAMPP):
- Download PCOV from PECL
- Extract
php_pcov.dlltoC:\laragon\bin\php\php-8.4.x\ext\ - Add to
php.ini:[pcov] extension=pcov pcov.enabled=1 pcov.directory=.
- Restart PHP:
php -m | grep pcov
macOS/Linux:
pecl install pcov
echo "extension=pcov.so" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||")Windows:
- Download from Xdebug.org
- Follow installation wizard instructions
- Add to
php.ini:[xdebug] zend_extension=xdebug xdebug.mode=coverage
macOS/Linux:
pecl install xdebug
echo "zend_extension=xdebug.so" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||")
echo "xdebug.mode=coverage" >> $(php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||")# HTML report (opens in browser)
composer test:coverage
# Text report in terminal
vendor/bin/phpunit --coverage-text
# XML report (for CI/CD)
vendor/bin/phpunit --coverage-clover coverage.xml
# All formats
vendor/bin/phpunit --coverage-html build/coverage \
--coverage-clover build/logs/clover.xml \
--coverage-textAfter running composer test:coverage:
- HTML Report:
build/coverage/index.html- Open in browser - XML Report:
build/logs/clover.xml- For CI/CD tools - Text Report: Displayed in terminal
The project enforces minimum coverage thresholds in phpunit.xml:
<coverage>
<report>
<html outputDirectory="build/coverage"/>
<clover outputFile="build/logs/clover.xml"/>
</report>
</coverage>Current Coverage Targets:
- Lines: 95%+
- Functions: 98%+
- Classes: 100%
Mutation testing validates the quality of your tests by intentionally introducing bugs (mutations) into your code and checking if your tests catch them.
Traditional code coverage only tells you which lines were executed, not if your tests actually validate the behavior. Mutation testing goes further:
- Infection modifies your source code (e.g., changes
*to+,==to!=) - Runs your test suite against the mutated code
- If tests fail → Good! Your tests caught the bug ✓
- If tests pass → Bad! Your tests didn't catch the bug ✗ (escaped mutant)
// Original code
public function getTotal(): float
{
return $this->price * $this->quantity; // Multiplication
}
// Mutation: * changed to +
public function getTotal(): float
{
return $this->price + $this->quantity; // Addition (BUG!)
}
// Good test (catches mutation):
$this->assertSame(100.0, $product->getTotal()); // ✓ Fails with mutation
// Bad test (doesn't catch mutation):
$this->assertIsFloat($product->getTotal()); // ✗ Still passes with mutationMutation testing requires PCOV or Xdebug for code coverage (see Code Coverage section).
# Verify coverage driver is installed
php -m | grep -E "pcov|xdebug"# Run mutation testing (recommended - shows mutations)
composer mutation
# Run with detailed report
composer mutation:report
# Run only on covered code (faster)
composer mutation:baseline
# Manual execution with custom options
vendor/bin/infection --threads=4 --show-mutations --min-msi=85After running composer mutation, you'll see output like:
735 mutations were generated:
519 mutants were killed
189 mutants were not covered by tests
21 covered mutants were not detected
6 errors were encountered
0 syntax errors were encountered
0 time outs were encountered
0 mutants required more time than configured
Metrics:
Mutation Score Indicator (MSI): 87%
Mutation Code Coverage: 74%
Covered Code MSI: 96%
1. Mutation Score Indicator (MSI)
- What: Percentage of killed mutants out of all mutants
- Formula: (Killed / Total) × 100
- Target: 85%+
- Meaning: Overall test effectiveness
2. Mutation Code Coverage
- What: Percentage of mutants covered by tests
- Formula: (Covered / Total) × 100
- Target: 80%+
- Meaning: How much code is reachable by tests
3. Covered Code MSI
- What: Percentage of killed mutants out of covered mutants
- Formula: (Killed / Covered) × 100
- Target: 90%+
- Meaning: Test quality for covered code
Infection applies various mutations (mutators):
// Arithmetic Operators
$a * $b → $a + $b // Multiplication to Addition
$a / $b → $a * $b // Division to Multiplication
$a % $b → $a * $b // Modulus to Multiplication
// Comparison Operators
$a == $b → $a != $b // Equal to Not Equal
$a < $b → $a <= $b // Less Than to Less Or Equal
$a > $b → $a >= $b // Greater Than to Greater Or Equal
// Logical Operators
$a && $b → $a || $b // AND to OR
!$a → $a // NOT removed
// Return Values
return true; → return false;
return $value; → return null;
// Increments
$i++; → $i--;
++$i; → --$i;
// Array Operations
$array[] → array_pop($array)
count($a) → count($a) + 1
// String Operations
$a . $b → $b // Concatenation removal
trim($s) → $s // Function call removalThe mutation testing configuration is in infection.json5:
{
// Minimum thresholds (fail if below)
"minMsi": 85, // Minimum Mutation Score Indicator
"minCoveredMsi": 90, // Minimum Covered Code MSI
// Mutators to apply
"mutators": {
"@default": true, // All default mutators
"@function_signature": false // Disable (too many false positives)
}
}After running mutation tests, check the generated reports:
# HTML report (detailed, interactive)
open build/infection/infection.html
# Text log
cat build/infection/infection.log
# Summary
cat build/infection/summary.log
# JSON (for CI/CD)
cat build/infection/infection.jsonIf you have escaped mutants (mutants not detected), improve your tests:
Example: Escaped Mutant
Escaped mutants:
===============
1) src/Util/Validator.php:42 [M] DecrementInteger
- return count($items) > 0;
+ return count($items) - 1 > 0;
Fix: Add specific assertion
// Before (weak test)
$this->assertTrue($validator->hasItems());
// After (strong test)
$this->assertTrue($validator->hasItems());
$this->assertSame(3, count($items)); // Catches count mutationsAdd to .github/workflows/mutation-tests.yml:
name: Mutation Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
mutation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: pcov
- run: composer install
- name: Run Mutation Tests
run: composer mutation:report
- name: Upload Mutation Report
uses: actions/upload-artifact@v4
if: always()
with:
name: mutation-report
path: build/infection/- ✅ Run regularly - Include in CI/CD pipeline
- ✅ Focus on critical code - Payment, validation, security logic
- ✅ Don't aim for 100% - 85-90% MSI is excellent
- ✅ Review escaped mutants - Learn from them, improve tests
- ✅ Use
--only-covered- Faster feedback loop during development - ✅ Disable problematic mutators - If too many false positives
⚠️ Expect longer runtime - 10-30 minutes for full run⚠️ Not a replacement - Complement to code coverage, not replacement
"The process has been signaled with signal '9'"
# Increase timeout
vendor/bin/infection --timeout=20"No mutations generated"
# Ensure tests have coverage
composer test:coverageToo many false positives from specific mutator
// infection.json5
"mutators": {
"@default": true,
"PregQuote": false, // Disable specific mutator
"ArrayItemRemoval": {
"ignore": [
"MyClass::myMethod" // Ignore specific method
]
}
}Tests are too slow
# Use more threads (use CPU core count)
vendor/bin/infection --threads=8
# Run only on changed files
vendor/bin/infection --git-diff-lines --git-diff-base=mainIntegration tests validate the client against the real Digistore24 API. Unlike unit tests which use mocks, integration tests make actual HTTP requests.
Some endpoints cost REAL MONEY!
Endpoints like createBillingOnDemand, createRebillingPayment, refundPurchase execute real transactions that:
- Charge real money (min €0.80 per test)
- Create real refunds
- Affect real customer accounts
Always use TEST data only!
Integration tests require configuration in .env.local:
-
Copy the example file:
cp .env.example .env.local
-
Fill in your test data:
# .env.local DS24_API_KEY=your-api-key-here DS24_TEST_PRODUCT_ID=12345 DS24_TEST_PURCHASE_ID=TESTORDER123 DS24_TEST_PURCHASE_WITH_REBILLING=REBILL456 # ... see .env.example for all options
-
Use TEST data only:
- Test products with €0.01 price
- Test purchases from test orders
- Test buyer accounts (not real customers)
# Run all integration tests (skips tests with missing config)
composer test:integration
# Run specific test file
vendor/bin/phpunit tests/Integration/BillingIntegrationTest.php
# Run with warnings suppressed
DS24_SUPPRESS_WARNINGS=1 composer test:integrationAutomatic Skipping:
- Tests automatically skip if required configuration is missing
- Clear warning message shows which config key is needed
- Summary at end shows all missing configurations
Example:
⚠️ Required configuration 'DS24_TEST_PRODUCT_ID' is not set
Please set 'DS24_TEST_PRODUCT_ID' in .env.local or environment variables.
See .env.example for all available configuration options.
Skipped: 1 test
See .env.example for complete list. Key configurations:
| Config Key | Purpose | Required For |
|---|---|---|
DS24_API_KEY |
API authentication | All tests |
DS24_TEST_PRODUCT_ID |
Product for testing | Product tests |
DS24_TEST_PURCHASE_ID |
Purchase for testing | Purchase tests |
DS24_TEST_PURCHASE_WITH_REBILLING |
Purchase with rebilling | Billing tests |
DS24_TEST_BUYER_EMAIL |
Test buyer email | Buyer tests |
Integration tests can be run manually via GitHub Actions:
- Go to Actions → Integration Tests
- Click Run workflow
- Configure inputs:
- Test Product ID: Your test product ID (optional)
- Test Purchase ID: Your test purchase ID (optional)
- Test Buyer Email: Your test buyer email (optional)
- etc.
- Click Run workflow
GitHub Secrets Required:
DS24_API_KEY- Your Digistore24 API key
Note: All test data is provided as workflow inputs, not secrets. This allows you to easily update test data without changing repository secrets.
- ✅ Always use TEST data
- ✅ Create dedicated test products with €0.01 price
- ✅ Use separate test account for integration testing
- ✅ Review all test data before running tests
- ✅ Check
.env.examplefor all required configurations ⚠️ Never commit.env.local(already in .gitignore)
Extend IntegrationTestCase for automatic configuration handling:
<?php
namespace GoSuccess\Digistore24\Api\Tests\Integration;
use PHPUnit\Framework\Attributes\Group;
#[Group('integration')]
class MyIntegrationTest extends IntegrationTestCase
{
public function testSomething(): void
{
// Get API key (skips test if not configured)
$apiKey = $this->getApiKey();
// Get required config (skips test if missing)
$productId = $this->requireConfig('DS24_TEST_PRODUCT_ID');
// Get optional config with default
$timeout = $this->getConfig('DS24_TIMEOUT', '30');
// Your test code here...
}
}Follow PHPUnit best practices:
<?php
namespace GoSuccess\Digistore24\Api\Tests\Unit;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(YourClass::class)]
class YourClassTest extends TestCase
{
private YourClass $subject;
protected function setUp(): void
{
parent::setUp();
$this->subject = new YourClass();
}
protected function tearDown(): void
{
parent::tearDown();
}
public function testItDoesSomething(): void
{
// Arrange
$input = 'test';
// Act
$result = $this->subject->doSomething($input);
// Assert
$this->assertSame('expected', $result);
}
}- Test classes:
{ClassName}Test.php - Test methods:
test{Behavior}()ortestIt{Does}{Something}() - Use descriptive names:
testThrowsExceptionWhenApiKeyIsEmpty()
Common PHPUnit assertions:
// Equality
$this->assertSame($expected, $actual); // Strict equality (===)
$this->assertEquals($expected, $actual); // Loose equality (==)
// Types
$this->assertIsString($value);
$this->assertIsInt($value);
$this->assertInstanceOf(ClassName::class, $object);
// Booleans
$this->assertTrue($condition);
$this->assertFalse($condition);
// Arrays
$this->assertCount(3, $array);
$this->assertContains('item', $array);
$this->assertArrayHasKey('key', $array);
// Exceptions
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Invalid API key');Use data providers for multiple test cases:
/**
* @dataProvider validEmailProvider
*/
public function testValidatesEmails(string $email, bool $expected): void
{
$result = $this->validator->isValid($email);
$this->assertSame($expected, $result);
}
public static function validEmailProvider(): array
{
return [
['test@example.com', true],
['invalid-email', false],
['', false],
];
}Use PHPUnit mocks for dependencies:
public function testCallsApiClient(): void
{
$mockClient = $this->createMock(ApiClient::class);
$mockClient->expects($this->once())
->method('post')
->with('/endpoint', ['data' => 'value'])
->willReturn(['result' => 'success']);
$service = new YourService($mockClient);
$result = $service->doSomething();
$this->assertSame('success', $result);
}Tests run automatically on every push and pull request:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: pcov
- run: composer install
- run: composer test:coverageCoverage badge in README.md:
Generated from coverage reports in CI/CD pipeline.
"No code coverage driver available"
# Install PCOV or Xdebug
pecl install pcov
php -m | grep pcov"Class not found"
# Regenerate autoloader
composer dump-autoload"Tests are slow"
# Use PCOV instead of Xdebug (5-10x faster)
# Or run without coverage:
composer test"Memory limit exceeded"
# Increase PHP memory limit
php -d memory_limit=512M vendor/bin/phpunit- ✅ Write tests first (TDD) or alongside feature code
- ✅ One assertion per test (ideally, or related assertions)
- ✅ Use descriptive test names that explain the behavior
- ✅ Keep tests simple - tests shouldn't need tests
- ✅ Mock external dependencies (HTTP, Database, etc.)
- ✅ Test edge cases (null, empty, invalid input)
- ✅ Aim for >95% coverage but focus on meaningful tests
- ✅ Run tests before commits (
composer check)