> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/appertafoundation/openeyes/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Testing approaches and best practices for OpenEyes development

OpenEyes uses a comprehensive testing approach combining PHPUnit for PHP tests and Cypress for end-to-end testing. This guide covers how to write and run tests.

## Overview

OpenEyes tests fall into two main categories:

1. **Sample Data Tests** - Use the sample database (recommended for new tests)
2. **Fixture-based Tests** - Use fixtures (legacy approach)

<Note>
  All new tests should be written to work with the sample database, as this allows developers to run tests locally without interfering with their development environment.
</Note>

## PHP Testing

### PHPUnit Configuration

OpenEyes uses PHPUnit 9.6.5 for PHP testing:

```json composer.json theme={null}
{
  "require-dev": {
    "phpunit/phpunit": "9.6.5",
    "phpunit/php-invoker": "^3.0",
    "fakerphp/faker": "^1.13"
  }
}
```

### Running Tests

Run tests tagged with the sample-data group:

```bash theme={null}
oe-unit-tests --group=sample-data
```

Run specific test groups:

```bash theme={null}
# Run feature tests
oe-unit-tests --group=feature

# Run profile tests
oe-unit-tests --group=profile

# Run specific test file
phpunit protected/tests/feature/ProfileTest.php
```

## Writing PHPUnit Tests

### Test Structure

Tests extend `OEDbTestCase` and use traits for functionality:

```php protected/tests/feature/ProfileTest.php theme={null}
<?php

use OE\factories\ModelFactory;

/**
 * @group sample-data
 * @group feature
 * @group profile
 */
class ProfileTest extends OEDbTestCase
{
    use \WithTransactions;
    use \MocksSession;
    use \MakesApplicationRequests;

    /** @test */
    public function only_runtime_selectable_firms_are_available()
    {
        list($user, $institution) = $this->createUserWithInstitution();

        $expected_firm = ModelFactory::factoryFor(Firm::class)->create([
            'runtime_selectable' => 1,
            'name' => 'Expected Foo',
            'institution_id' => $institution->id
        ]);

        $response = $this->actingAs($user, $institution)
            ->get('/profile/firms')
            ->assertSuccessful()
            ->crawl();

        $this->assertCount(
            1,
            $response->filter('[data-test="unselected_firms"] select option[value="' . $expected_firm->id . '"]')
        );
    }
}
```

### Required Annotations

All new tests should include the `@group sample-data` annotation:

```php theme={null}
/**
 * @group sample-data
 * @group feature
 * @group your-feature-name
 */
class YourTest extends OEDbTestCase
{
    // Test methods
}
```

## Model Factories

Model factories provide a simple way to generate test data.

### Using Factories

Create test instances with default values:

```php theme={null}
use OE\factories\ModelFactory;

// Create and save a patient
$patient = ModelFactory::for(Patient::class)->create();
$this->assertNotNull($patient->dob);

// Create with specific attributes
$patient = ModelFactory::for(Patient::class)->create(['dob' => '2005-03-05']);
$this->assertEquals(10, $patient->ageOn('2015-03-05'));

// Make without saving
$patient = ModelFactory::for(Patient::class)->make();
$this->assertNull($patient->id);
```

### Factory Shorthand

Models with the `HasFactory` trait support shorthand syntax:

```php theme={null}
// Using the trait
use OE\factories\models\traits\HasFactory;

class Patient extends BaseActiveRecordVersioned
{
    use HasFactory;
}

// Shorthand usage
$patient = Patient::factory()->create();
```

### Creating Factories

Define a factory for your model:

```php protected/factories/models/AddressFactory.php theme={null}
<?php
namespace OE\factories\models;

use OE\factories\ModelFactory;
use Contact;
use Country;

class AddressFactory extends ModelFactory
{
    public function definition(): array
    {
        return [
            'address1' => $this->faker->streetAddress(),
            'city' => $this->faker->city(),
            'postcode' => $this->faker->postcode(),
            'country_id' => ModelFactory::factoryFor(Country::class)
                ->useExisting(['code' => 'GB']),
            'contact_id' => Contact::factory()
        ];
    }

    public function full(): self
    {
        return $this->state([
            'address1' => $this->faker->secondaryAddress(),
            'address2' => $this->faker->streetAddress(),
            'county' => $this->faker->county()
        ]);
    }
}
```

### Factory Dependencies

Define dependent relations as factory calls:

```php theme={null}
class PatientFactory extends ModelFactory
{
    public function definition(): array
    {
        return [
            'first_name' => $this->faker->firstName(),
            'last_name' => $this->faker->lastName(),
            'contact_id' => ModelFactory::for(Contact::class)
        ];
    }
}
```

### Factory States

Define multiple states for different scenarios:

```php theme={null}
class PatientFactory extends ModelFactory
{
    public function male()
    {
        return $this->state(function () {
            return [
                'gender' => 'M',
                'contact_id' => ModelFactory::factoryFor(Contact::class)->male()
            ];
        });
    }

    public function female()
    {
        return $this->state([
            'gender' => 'F',
            'contact_id' => ModelFactory::factoryFor(Contact::class)->female()
        ]);
    }
}

// Usage
$patient = ModelFactory::for(Patient::class)->male()->create();

// States can be chained (later states override earlier ones)
$patient = ModelFactory::for(Patient::class)->male()->female()->create();
// This patient will be female
```

## Event Factories

Event factories create complete events with elements:

```php theme={null}
use OE\factories\EventFactory;

// Create a complete CVI event
$event = EventFactory::for('OphCoCvi')->create();
$this->assertTrue($event->patient instanceof Patient::class);
```

Event factories depend on element model factories being implemented.

## Test Traits

### WithTransactions

Wraps each test in a database transaction:

```php theme={null}
class MyTest extends OEDbTestCase
{
    use \WithTransactions;

    // Each test runs in a transaction and is rolled back
}
```

### MocksSession

Provides session mocking utilities:

```php theme={null}
use \MocksSession;

$this->mockSession([
    'user_id' => $user->id,
]);
```

### MakesApplicationRequests

Simplifies making HTTP requests:

```php theme={null}
use \MakesApplicationRequests;

$response = $this->actingAs($user, $institution)
    ->get('/profile/firms')
    ->assertSuccessful();
```

## Cypress E2E Testing

### Configuration

Cypress is configured for end-to-end testing:

```javascript cypress.config.js theme={null}
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost',
    viewportWidth: 1280,
    viewportHeight: 737,
    defaultCommandTimeout: 7000,
    retries: {
      runMode: 2,
      openMode: 0
    }
  },
});
```

### Running Cypress Tests

```bash theme={null}
# Open Cypress UI
npx cypress open

# Run tests headlessly
npx cypress run

# Run specific test file
npx cypress run --spec "cypress/e2e/admin/clinical-pathway-presets.cy.js"
```

### Writing Cypress Tests

Cypress tests use a BDD-style syntax:

```javascript cypress/e2e/admin/clinical-pathway-presets.cy.js theme={null}
describe('behaviour of the admin screen for clinical pathway presets', function () {
    beforeEach(() => {
        cy.createModels("PathwayType").as("pathway_type1");
        cy.createModels("PathwayType").as("pathway_type2");
        cy.login();
        cy.visit("/Admin/worklist/presetPathways");
    });

    it('toggles the activation status', function() {
        cy.getBySel("checkbox-" + this.pathway_type1.id).click();
        cy.getBySel("toggle-active-btn").click();
        cy.intercept("togglePathwayPresetsActivationStatus");
        cy.getBySel("is-active-" + this.pathway_type1.id).should("not.exist");
        cy.getBySel("is-active-" + this.pathway_type2.id).should("exist");
    });
});
```

### Custom Cypress Commands

OpenEyes provides custom commands:

```javascript theme={null}
// Create models using factories
cy.createModels("Patient").as("patient");

// Login
cy.login();

// Select by data-test attribute
cy.getBySel("button-id").click();
```

## Code Quality Tools

### PHP CodeSniffer

Check code style compliance:

```bash theme={null}
vendor/bin/phpcs --standard=phpcs.xml protected/modules/YourModule
```

OpenEyes follows PSR-12 with exceptions:

```xml phpcs.xml theme={null}
<rule ref="PSR12">
    <exclude name="Squiz.Classes.ValidClassName.NotCamelCaps" />
</rule>

<rule ref="Generic.Files.LineLength">
    <properties>
        <property name="lineLimit" value="150" />
    </properties>
</rule>
```

### PHPStan

Run static analysis:

```bash theme={null}
vendor/bin/phpstan analyse
```

Configuration:

```yaml phpstan.neon theme={null}
parameters:
  level: 0
  phpVersion: 80000
  scanDirectories:
    - vendor/yiisoft/yii/framework
    - protected/modules
```

### PHP CS Fixer

Automatically fix code style:

```bash theme={null}
vendor/bin/php-cs-fixer fix protected/modules/YourModule
```

## Test Organization

### Directory Structure

```
protected/tests/
├── api/                    # API tests
├── feature/                # Feature tests
├── unit/                   # Unit tests
└── fixtures/              # Test fixtures (legacy)

protected/modules/OphCiExamination/tests/
├── unit/                   # Module unit tests
└── feature/                # Module feature tests
```

### Test Naming

Follow these conventions:

* Test files end with `Test.php`
* Test methods start with `test` or use `/** @test */`
* Use descriptive names: `test_user_can_create_event()`

## Best Practices

1. **Tag all tests** with `@group sample-data` and relevant groups
2. **Use transactions** with `WithTransactions` trait for database tests
3. **Create factories** for all models to support testing
4. **Test isolation** - Each test should be independent
5. **Use factories** instead of fixtures for new tests
6. **Mock external dependencies** to keep tests fast
7. **Test edge cases** and error conditions
8. **Keep tests simple** - One assertion per test when possible

## Continuous Integration

Tests run automatically in CI pipelines. Ensure:

* All tests pass before committing
* No skipped tests without good reason
* Code style checks pass
* PHPStan analysis passes

## Debugging Tests

### PHPUnit Debugging

```bash theme={null}
# Run with verbose output
oe-unit-tests --group=sample-data --verbose

# Stop on first failure
oe-unit-tests --group=sample-data --stop-on-failure

# Run specific test method
oe-unit-tests --filter=test_user_can_create_event
```

### Cypress Debugging

```javascript theme={null}
// Use cy.debug() to pause
cy.getBySel("button").debug().click();

// Use cy.pause() for interactive debugging
cy.pause();

// Screenshot on failure (automatic)
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Contributing" icon="code-pull-request" href="/development/contributing">
    Learn how to contribute your code
  </Card>

  <Card title="Module Development" icon="puzzle-piece" href="/development/modules">
    Create custom modules
  </Card>
</CardGroup>
