Skip to content

Remove the need for AbstractStartedContainer wrappers in modules #1563

Open

Description

@cristianrgreco

What happens

Every module is two classes joined by a forwarding wrapper. Simplified from the redis module:

export class RedisContainer extends GenericContainer {
  public override async start(): Promise<StartedRedisContainer> {
    return new StartedRedisContainer(await super.start(), this.password);
  }
}

export class StartedRedisContainer extends AbstractStartedContainer {
  constructor(startedTestContainer: StartedTestContainer, private readonly password?: string) {
    super(startedTestContainer);
  }
}

AbstractStartedContainer holds a StartedTestContainer and forwards each of its 20 methods to it.

  • 50 module classes extend it.
  • 50 module files override start(), and each override has to wrap the result of super.start() in the module's started class.
  • Every method added to StartedTestContainer has to be written three times: on the interface, on StartedGenericContainer and as a forwarder on AbstractStartedContainer.
  • Forwarding has caused bugs: AbstractStartedContainer asyncDispose should call this.stop #1202, where Symbol.asyncDispose called the inner stop() and skipped the subclass's.
  • There are two sets of stop hooks: containerStopping/containerStopped on AbstractStartedContainer, and containerIsStopping/containerIsStopped on StartedGenericContainer, which nothing overrides. That class is not exported.
  • AbstractStoppedContainer repeats the pattern and is used once (selenium).
  • Overriding start() to wrap is also where modules put configuration, which is why start() is not repeatable (Make start() safe to call more than once on the same container #1520).

Expected

A module author declares the started type once, with no forwarding class and no start() override just to wrap. Some directions:

  • Make GenericContainer generic over its started type, with a protected factory that the base start() calls. Configuration moves to a hook such as beforeContainerCreated.
  • Export a started base class that module containers extend directly, and let the builder say which class to construct. Inheritance replaces forwarding.
  • One class for both states, as Java and Go have.

Where

  • packages/testcontainers/src/generic-container/abstract-started-container.ts
  • packages/testcontainers/src/generic-container/abstract-stopped-container.ts
  • packages/testcontainers/src/generic-container/started-generic-container.ts
  • every packages/modules/*/src/*-container.ts

Notes

  • Java and Go use a single type for a configured and a running container. .NET splits builder and container like this repo, but the module container extends the base container directly (PostgreSqlContainer : DockerContainer, IDatabaseContainer), with no forwarding layer.
  • Compose and the reuse path construct StartedGenericContainer themselves, so the design has to cover those.
  • Semver: labelled major for removing AbstractStartedContainer. The replacement can ship first in a minor, with the wrapper deprecated.

Dependencies

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmajorAn incompatible API change

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions