Moko Consulting

Open-source software for Joomla, Gitea, and web platforms. Home of MokoSuite, MokoGitea, and MokoCLI.

Tennessee
standards/testing

Testing Standards

Testing expectations across all MokoConsulting projects.

PHP (Joomla Extensions)

Framework

PHPUnit 10+ with Joomla's testing framework.

Project Setup

tests/
├── Unit/
│   ├── Engine/
│   │   └── BackupEngineTest.php
│   └── Model/
│       └── ProfilesModelTest.php
├── Integration/
│   └── Api/
│       └── BackupApiTest.php
└── bootstrap.php

Configuration

phpunit.xml in repo root:

<phpunit bootstrap="tests/bootstrap.php" colors="true">
  <testsuites>
    <testsuite name="Unit">
      <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="Integration">
      <directory>tests/Integration</directory>
    </testsuite>
  </testsuites>
</phpunit>

Conventions

  • Test class name matches source: BackupEngineBackupEngineTest
  • Test methods: test{MethodName}{Scenario} or test_{method}_{scenario}
  • One assertion concept per test method
  • Use data providers for multiple input scenarios
  • No mocking the database — use real database for integration tests
public function testRunBackupCreatesArchive(): void
{
    $engine = new BackupEngine($this->db, $this->tempDir);
    $result = $engine->run($this->profile);

    $this->assertFileExists($result->archivePath);
    $this->assertGreaterThan(0, $result->sizeBytes);
    $this->assertSame(BackupStatus::Complete, $result->status);
}

/**
 * @dataProvider invalidProfileProvider
 */
public function testRunBackupRejectsInvalidProfile(array $config): void
{
    $this->expectException(BackupException::class);
    $engine = new BackupEngine($this->db, $this->tempDir);
    $engine->run(new Profile($config));
}

Go (MokoGitea)

  • Standard testing package with testify/assert
  • Table-driven tests for all functions with multiple cases
  • go test -race ./... must pass
  • Integration tests tagged with //go:build integration
func TestParseManifest(t *testing.T) {
    tests := []struct {
        name    string
        xml     string
        want    *Manifest
        wantErr bool
    }{
        {"valid joomla", `<manifest><platform>joomla</platform></manifest>`, &Manifest{Platform: "joomla"}, false},
        {"empty", "", nil, true},
        {"invalid xml", "<broken", nil, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseManifest([]byte(tt.xml))
            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
                assert.Equal(t, tt.want.Platform, got.Platform)
            }
        })
    }
}

TypeScript (MCP Servers)

  • Jest or Vitest for unit tests
  • Tests in __tests__/ or *.test.ts alongside source
  • Mock external HTTP calls, not internal logic
  • Test tool handlers with sample inputs
describe('backup tool', () => {
  it('returns success for valid target', async () => {
    const result = await handleBackupRun({ target: 'gitea-db' });
    expect(result.content[0].text).toContain('Backup started');
  });

  it('rejects unknown target', async () => {
    await expect(handleBackupRun({ target: 'invalid' }))
      .rejects.toThrow('Unknown backup target');
  });
});

Coverage Expectations

  • New code: 80% line coverage minimum
  • Critical paths (security, data integrity): 100% coverage
  • Bug fixes: must include regression test
  • Coverage is informational, not a gate — meaningful tests over coverage targets

Running Tests

# PHP
make test                 # or: vendor/bin/phpunit

# Go
go test ./...

# TypeScript
npm test                  # or: npx jest

All tests run in CI on every PR. Merging requires green CI.