second first commit
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
# Testcontainers for .NET
|
||||
|
||||
The `Testcontainers.Floci` package integrates Floci with [Testcontainers for .NET](https://dotnet.testcontainers.org/). It starts a real Floci container before your tests and disposes it after, and gives you a typed, per-service configuration API over Floci's environment-variable surface.
|
||||
|
||||
Targets `net8.0`, `net9.0`, `net10.0`, `netstandard2.0` and `netstandard2.1`.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
dotnet add package Testcontainers.Floci
|
||||
```
|
||||
|
||||
!!! note "Published to GitHub Packages, not nuget.org"
|
||||
|
||||
The package is served from the [floci-io GitHub Packages feed](https://github.com/floci-io/testcontainers-floci-dotnet/pkgs/nuget/Testcontainers.Floci). A package with the same id, `Testcontainers.Floci`, also exists on nuget.org, published by the Testcontainers project. Without package source mapping, NuGet may resolve that one instead of this module, so the mapping block below is required, not optional. Add a `nuget.config` next to your solution:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="github-floci-io" value="https://nuget.pkg.github.com/floci-io/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceCredentials>
|
||||
<github-floci-io>
|
||||
<add key="Username" value="%GITHUB_ACTOR%" />
|
||||
<add key="ClearTextPassword" value="%GITHUB_PACKAGES_PAT%" />
|
||||
</github-floci-io>
|
||||
</packageSourceCredentials>
|
||||
<packageSourceMapping>
|
||||
<!-- Route only this package to the floci-io feed; everything else stays on nuget.org. -->
|
||||
<packageSource key="github-floci-io">
|
||||
<package pattern="Testcontainers.Floci" />
|
||||
</packageSource>
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
GitHub Packages requires authentication even for public packages. `GITHUB_PACKAGES_PAT` is a personal access token with the `read:packages` scope, and `GITHUB_ACTOR` is your GitHub username. The `dotnet nuget add source` command can register the feed and credentials for you, but it does not create the `packageSourceMapping` entry, so the config file above is the canonical setup.
|
||||
|
||||
## Basic usage with xUnit
|
||||
|
||||
The container implements the Testcontainers builder pattern. Use `IAsyncLifetime` so it starts once per test class:
|
||||
|
||||
```csharp
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Testcontainers.Floci;
|
||||
using Xunit;
|
||||
|
||||
public sealed class S3Tests : IAsyncLifetime
|
||||
{
|
||||
private readonly FlociContainer _floci = new FlociBuilder("floci/floci:latest").Build();
|
||||
|
||||
public Task InitializeAsync() => _floci.StartAsync();
|
||||
|
||||
public Task DisposeAsync() => _floci.DisposeAsync().AsTask();
|
||||
|
||||
[Fact]
|
||||
public async Task CreatesAndListsBucket()
|
||||
{
|
||||
using var s3 = new AmazonS3Client(
|
||||
_floci.AccessKey,
|
||||
_floci.SecretKey,
|
||||
new AmazonS3Config
|
||||
{
|
||||
ServiceURL = _floci.GetEndpoint(),
|
||||
AuthenticationRegion = _floci.Region,
|
||||
ForcePathStyle = true,
|
||||
});
|
||||
|
||||
await s3.PutBucketAsync(new PutBucketRequest { BucketName = "my-bucket" });
|
||||
|
||||
var buckets = await s3.ListBucketsAsync();
|
||||
Assert.Contains(buckets.Buckets, b => b.BucketName == "my-bucket");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`GetEndpoint()` returns `http://localhost:<mapped-port>/`. `Region`, `AccountId`, `AvailabilityZone`, `AccessKey` and `SecretKey` expose the container's identity defaults.
|
||||
|
||||
## SQS example
|
||||
|
||||
```csharp
|
||||
using Amazon.SQS;
|
||||
using Amazon.SQS.Model;
|
||||
using Testcontainers.Floci;
|
||||
using Xunit;
|
||||
|
||||
public sealed class SqsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly FlociContainer _floci = new FlociBuilder("floci/floci:latest").Build();
|
||||
|
||||
public Task InitializeAsync() => _floci.StartAsync();
|
||||
|
||||
public Task DisposeAsync() => _floci.DisposeAsync().AsTask();
|
||||
|
||||
private AmazonSQSClient CreateClient() => new(
|
||||
_floci.AccessKey,
|
||||
_floci.SecretKey,
|
||||
new AmazonSQSConfig
|
||||
{
|
||||
ServiceURL = _floci.GetEndpoint(),
|
||||
AuthenticationRegion = _floci.Region,
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task SendsAndReceivesMessage()
|
||||
{
|
||||
using var sqs = CreateClient();
|
||||
|
||||
var queueUrl = (await sqs.CreateQueueAsync(
|
||||
new CreateQueueRequest { QueueName = "orders" })).QueueUrl;
|
||||
|
||||
await sqs.SendMessageAsync(new SendMessageRequest
|
||||
{
|
||||
QueueUrl = queueUrl,
|
||||
MessageBody = """{"event":"order.placed"}""",
|
||||
});
|
||||
|
||||
var received = await sqs.ReceiveMessageAsync(new ReceiveMessageRequest
|
||||
{
|
||||
QueueUrl = queueUrl,
|
||||
MaxNumberOfMessages = 1,
|
||||
});
|
||||
|
||||
Assert.Single(received.Messages);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DynamoDB example
|
||||
|
||||
```csharp
|
||||
using Amazon.DynamoDBv2;
|
||||
using Amazon.DynamoDBv2.Model;
|
||||
using Testcontainers.Floci;
|
||||
using Xunit;
|
||||
|
||||
public sealed class DynamoDbTests : IAsyncLifetime
|
||||
{
|
||||
private readonly FlociContainer _floci = new FlociBuilder("floci/floci:latest").Build();
|
||||
|
||||
public Task InitializeAsync() => _floci.StartAsync();
|
||||
|
||||
public Task DisposeAsync() => _floci.DisposeAsync().AsTask();
|
||||
|
||||
[Fact]
|
||||
public async Task PutsAndGetsItem()
|
||||
{
|
||||
using var dynamo = new AmazonDynamoDBClient(
|
||||
_floci.AccessKey,
|
||||
_floci.SecretKey,
|
||||
new AmazonDynamoDBConfig
|
||||
{
|
||||
ServiceURL = _floci.GetEndpoint(),
|
||||
AuthenticationRegion = _floci.Region,
|
||||
});
|
||||
|
||||
await dynamo.CreateTableAsync(new CreateTableRequest
|
||||
{
|
||||
TableName = "Orders",
|
||||
AttributeDefinitions = [new AttributeDefinition("id", ScalarAttributeType.S)],
|
||||
KeySchema = [new KeySchemaElement("id", KeyType.HASH)],
|
||||
BillingMode = BillingMode.PAY_PER_REQUEST,
|
||||
});
|
||||
|
||||
await dynamo.PutItemAsync(new PutItemRequest
|
||||
{
|
||||
TableName = "Orders",
|
||||
Item = new()
|
||||
{
|
||||
["id"] = new AttributeValue { S = "order-1" },
|
||||
["status"] = new AttributeValue { S = "placed" },
|
||||
},
|
||||
});
|
||||
|
||||
var response = await dynamo.GetItemAsync(new GetItemRequest
|
||||
{
|
||||
TableName = "Orders",
|
||||
Key = new() { ["id"] = new AttributeValue { S = "order-1" } },
|
||||
});
|
||||
|
||||
Assert.Equal("placed", response.Item["status"].S);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuring services
|
||||
|
||||
Each service has a `record` config with `init` properties. Pass it to the matching `With…` method on the builder; only the settings you set differ from Floci's defaults.
|
||||
|
||||
```csharp
|
||||
var floci = new FlociBuilder("floci/floci:latest")
|
||||
.WithRegion("eu-west-2")
|
||||
.WithAccountId("123456789012")
|
||||
.WithSqs(new SqsConfig { VisibilityTimeout = 60, MaxMessageSize = 131072 })
|
||||
.WithS3(new S3Config { Enabled = true })
|
||||
.Build();
|
||||
```
|
||||
|
||||
Configs map onto Floci's `FLOCI_SERVICES_<SERVICE>_<SETTING>` environment variables. `ENABLED` is always sent; the rest only when the service is enabled.
|
||||
|
||||
## Container-based services
|
||||
|
||||
RDS, Lambda, ElastiCache, ECS, EC2 and ECR make Floci spawn real sibling containers through the Docker daemon. Their configs opt into mounting the Docker socket and into publishing ports 1:1, because Floci returns `endpoint=localhost:<port>` literally.
|
||||
|
||||
```csharp
|
||||
var floci = new FlociBuilder("floci/floci:latest")
|
||||
.WithRds(new RdsConfig { Enabled = true, ProxyBasePort = 7010 })
|
||||
.Build();
|
||||
```
|
||||
|
||||
A few things to know:
|
||||
|
||||
- Sibling containers are managed by Floci, not by Ryuk. They are named after the resource, so delete the resource in teardown (`DeleteDBInstanceAsync`, `DeleteFunctionAsync`, `DeleteReplicationGroupAsync`) or the container is left behind.
|
||||
- **On macOS, avoid the default RDS `ProxyBasePort` of 7000**: AirPlay Receiver listens on `*:7000`. Use 7010, as above.
|
||||
- Connect to `127.0.0.1` rather than `localhost` so the client does not resolve to IPv6, and disable SSL (`SSL Mode=Disable`): Floci's RDS proxy does not negotiate TLS.
|
||||
- The first Lambda invocation pulls the AWS runtime image and cold-starts (roughly 8–10s). Give the client a generous timeout.
|
||||
|
||||
## Running the tests
|
||||
|
||||
Integration tests need a running Docker daemon.
|
||||
|
||||
```bash
|
||||
dotnet build # must stay warning-free
|
||||
dotnet test # requires Docker
|
||||
```
|
||||
|
||||
Under [Colima](https://github.com/abiosoft/colima) on macOS, the host socket path cannot be bind-mounted into a container, which breaks Ryuk. Point Testcontainers at the in-VM path instead, with no code change required:
|
||||
|
||||
```bash
|
||||
export DOCKER_HOST="unix:///Users/<you>/.colima/default/docker.sock"
|
||||
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE="/var/run/docker.sock"
|
||||
```
|
||||
|
||||
Container-backed tests can flake when many Floci containers start at once, so the suite disables xUnit's test parallelization.
|
||||
|
||||
## Source and changelog
|
||||
|
||||
[github.com/floci-io/testcontainers-floci-dotnet](https://github.com/floci-io/testcontainers-floci-dotnet)
|
||||
@@ -0,0 +1,287 @@
|
||||
# Testcontainers — Go
|
||||
|
||||
The `testcontainers-floci-go` module integrates Floci with [Testcontainers for Go](https://golang.testcontainers.org/). It starts a real Floci container before your tests and shuts it down after, with no extra setup.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/floci-io/testcontainers-floci-go
|
||||
```
|
||||
|
||||
Requires Go 1.25+ and Testcontainers for Go v0.42+.
|
||||
|
||||
## Basic usage
|
||||
|
||||
```go
|
||||
package myservice_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
floci "github.com/floci-io/testcontainers-floci-go"
|
||||
)
|
||||
|
||||
func TestS3CreateBucket(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
container, err := floci.NewFlociContainer().Start(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer container.Stop(ctx)
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(ctx,
|
||||
config.WithRegion(container.GetRegion()),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
container.GetAccessKey(), container.GetSecretKey(), "",
|
||||
)),
|
||||
config.WithBaseEndpoint(container.GetEndpoint()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = true
|
||||
})
|
||||
|
||||
_, err = client.CreateBucket(ctx, &s3.CreateBucketInput{
|
||||
Bucket: aws.String("my-bucket"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := client.ListBuckets(ctx, &s3.ListBucketsInput{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, b := range out.Buckets {
|
||||
if aws.ToString(b.Name) == "my-bucket" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("bucket not found after create")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SQS example
|
||||
|
||||
```go
|
||||
func TestSqsSendReceive(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
container, err := floci.NewFlociContainer().Start(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer container.Stop(ctx)
|
||||
|
||||
cfg, _ := config.LoadDefaultConfig(ctx,
|
||||
config.WithRegion(container.GetRegion()),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
container.GetAccessKey(), container.GetSecretKey(), "",
|
||||
)),
|
||||
config.WithBaseEndpoint(container.GetEndpoint()),
|
||||
)
|
||||
|
||||
client := sqs.NewFromConfig(cfg)
|
||||
|
||||
queue, err := client.CreateQueue(ctx, &sqs.CreateQueueInput{
|
||||
QueueName: aws.String("orders"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.SendMessage(ctx, &sqs.SendMessageInput{
|
||||
QueueUrl: queue.QueueUrl,
|
||||
MessageBody: aws.String(`{"event":"order.placed"}`),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
|
||||
QueueUrl: queue.QueueUrl,
|
||||
MaxNumberOfMessages: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(out.Messages) != 1 {
|
||||
t.Fatalf("expected 1 message, got %d", len(out.Messages))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DynamoDB example
|
||||
|
||||
```go
|
||||
func TestDynamoDBPutGet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
container, err := floci.NewFlociContainer().Start(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer container.Stop(ctx)
|
||||
|
||||
cfg, _ := config.LoadDefaultConfig(ctx,
|
||||
config.WithRegion(container.GetRegion()),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
container.GetAccessKey(), container.GetSecretKey(), "",
|
||||
)),
|
||||
config.WithBaseEndpoint(container.GetEndpoint()),
|
||||
)
|
||||
|
||||
client := dynamodb.NewFromConfig(cfg)
|
||||
|
||||
_, err = client.CreateTable(ctx, &dynamodb.CreateTableInput{
|
||||
TableName: aws.String("Orders"),
|
||||
BillingMode: types.BillingModePayPerRequest,
|
||||
AttributeDefinitions: []types.AttributeDefinition{
|
||||
{AttributeName: aws.String("id"), AttributeType: types.ScalarAttributeTypeS},
|
||||
},
|
||||
KeySchema: []types.KeySchemaElement{
|
||||
{AttributeName: aws.String("id"), KeyType: types.KeyTypeHash},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.PutItem(ctx, &dynamodb.PutItemInput{
|
||||
TableName: aws.String("Orders"),
|
||||
Item: map[string]types.AttributeValue{
|
||||
"id": &types.AttributeValueMemberS{Value: "order-1"},
|
||||
"status": &types.AttributeValueMemberS{Value: "placed"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
||||
TableName: aws.String("Orders"),
|
||||
Key: map[string]types.AttributeValue{
|
||||
"id": &types.AttributeValueMemberS{Value: "order-1"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
status := result.Item["status"].(*types.AttributeValueMemberS).Value
|
||||
if status != "placed" {
|
||||
t.Errorf("expected status 'placed', got '%s'", status)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reusing the container across tests
|
||||
|
||||
Start the container once with `TestMain` and share it across all tests in the package:
|
||||
|
||||
```go
|
||||
var sharedContainer *floci.StartedFlociContainer
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := context.Background()
|
||||
|
||||
var err error
|
||||
sharedContainer, err = floci.NewFlociContainer().Start(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to start floci: %v", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
sharedContainer.Stop(ctx)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func awsCfg(ctx context.Context) aws.Config {
|
||||
cfg, _ := config.LoadDefaultConfig(ctx,
|
||||
config.WithRegion(sharedContainer.GetRegion()),
|
||||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
|
||||
sharedContainer.GetAccessKey(), sharedContainer.GetSecretKey(), "",
|
||||
)),
|
||||
config.WithBaseEndpoint(sharedContainer.GetEndpoint()),
|
||||
)
|
||||
return cfg
|
||||
}
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
Options are chained on the builder before calling `Start`:
|
||||
|
||||
```go
|
||||
container, err := floci.NewFlociContainer().
|
||||
WithRegion("eu-west-1").
|
||||
WithAccountID("123456789012").
|
||||
WithImage("floci/floci:1.6.0").
|
||||
WithDedicatedNetwork().
|
||||
Start(ctx)
|
||||
```
|
||||
|
||||
| Method | Default | Description |
|
||||
|---|---|---|
|
||||
| `WithImage(image string)` | `floci/floci:latest` | Docker image to use |
|
||||
| `WithRegion(region string)` | `us-east-1` | AWS region set in Floci and returned by `GetRegion()` |
|
||||
| `WithAccountID(id string)` | `000000000000` | Default AWS account ID used in ARNs |
|
||||
| `WithAvailabilityZone(az string)` | `us-east-1a` | Availability zone reported by Floci |
|
||||
| `WithDedicatedNetwork()` | _(none)_ | Create a dedicated Docker network for container-backed services (ElastiCache, RDS, OpenSearch, MSK) |
|
||||
|
||||
## Service configuration
|
||||
|
||||
Each service exposes a typed config struct. Pass it with the matching `With<Service>Config` method:
|
||||
|
||||
```go
|
||||
container, err := floci.NewFlociContainer().
|
||||
WithS3Config(floci.S3Config{
|
||||
Enabled: true,
|
||||
DefaultPresignExpirySeconds: 7200,
|
||||
}).
|
||||
WithSqsConfig(floci.SqsConfig{
|
||||
Enabled: true,
|
||||
DefaultVisibilityTimeout: 60,
|
||||
MaxMessageSize: 131072,
|
||||
}).
|
||||
WithDynamoDbConfig(floci.DynamoDbConfig{
|
||||
Enabled: true,
|
||||
}).
|
||||
Start(ctx)
|
||||
```
|
||||
|
||||
Available config methods: `WithAcmConfig`, `WithApiGatewayConfig`, `WithAppConfigConfig`, `WithAthenaConfig`, `WithBackupConfig`, `WithBedrockRuntimeConfig`, `WithCloudFormationConfig`, `WithCloudWatchLogsConfig`, `WithCloudWatchMetricsConfig`, `WithCodeBuildConfig`, `WithCodeDeployConfig`, `WithCognitoConfig`, `WithDynamoDbConfig`, `WithEc2Config`, `WithEcrConfig`, `WithEcsConfig`, `WithEksConfig`, `WithElastiCacheConfig`, `WithElbConfig`, `WithEventBridgeConfig`, `WithFirehoseConfig`, `WithGlueConfig`, `WithIamConfig`, `WithKinesisConfig`, `WithKmsConfig`, `WithLambdaConfig`, `WithMskConfig`, `WithOpenSearchConfig`, `WithRdsConfig`, `WithRoute53Config`, `WithS3Config`, `WithSchedulerConfig`, `WithSecretsManagerConfig`, `WithSesConfig`, `WithSnsConfig`, `WithSqsConfig`, `WithSsmConfig`, `WithStepFunctionsConfig`, `WithStsConfig`, `WithTextractConfig`, `WithTransferConfig`.
|
||||
|
||||
## `StartedFlociContainer` API
|
||||
|
||||
| Method | Returns | Description |
|
||||
|---|---|---|
|
||||
| `GetEndpoint()` | `string` | Full HTTP endpoint, e.g. `http://localhost:32768` |
|
||||
| `GetRegion()` | `string` | AWS region configured at start |
|
||||
| `GetAccountID()` | `string` | AWS account ID configured at start |
|
||||
| `GetAccessKey()` | `string` | Always `"test"` |
|
||||
| `GetSecretKey()` | `string` | Always `"test"` |
|
||||
| `GetAvailabilityZone()` | `string` | Availability zone configured at start |
|
||||
| `GetDedicatedNetworkName()` | `string` | Docker network name, or empty string if not using a dedicated network |
|
||||
| `GetMappedPort(ctx, port int)` | `(int, error)` | Host port mapped to the given container port |
|
||||
| `Stop(ctx)` | `error` | Terminate the container and clean up |
|
||||
|
||||
## Source
|
||||
|
||||
[github.com/floci-io/testcontainers-floci-go](https://github.com/floci-io/testcontainers-floci-go)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Testcontainers
|
||||
|
||||
Floci has first-class Testcontainers modules for every major SDK language. Each module starts a real Floci container before your tests run and tears it down after — no running daemon, no shared state, no port conflicts.
|
||||
|
||||
## Available modules
|
||||
|
||||
For Testcontainers 1.x, use the versions as indicated in the table below.
|
||||
|
||||
| Language | Package | Version | Registry | Source |
|
||||
|---|---|---|---|---|
|
||||
| Java | `io.floci:testcontainers-floci` | `1.14.0` | [Maven Central](https://mvnrepository.com/artifact/io.floci/testcontainers-floci) | [GitHub](https://github.com/floci-io/testcontainers-floci) |
|
||||
| Node.js | `@floci/testcontainers` | `0.1.0` | [npm](https://www.npmjs.com/package/@floci/testcontainers) | [GitHub](https://github.com/floci-io/testcontainers-floci-node) |
|
||||
| Python | `testcontainers-floci` | `0.1.1` | [PyPI](https://pypi.org/project/testcontainers-floci/) | [GitHub](https://github.com/floci-io/testcontainers-floci-python) |
|
||||
| .NET | `Testcontainers.Floci` | see releases | [GitHub Packages](https://github.com/orgs/floci-io/packages?repo_name=testcontainers-floci-dotnet) | [GitHub](https://github.com/floci-io/testcontainers-floci-dotnet) |
|
||||
| Go | — | 🚧 In progress | — | [GitHub](https://github.com/floci-io/testcontainers-floci-go) |
|
||||
|
||||
For Testcontainers 2.x / Spring Boot 4.x, use version `2.15.0`.
|
||||
|
||||
## How it works
|
||||
|
||||
Every module exposes a `FlociContainer` class that wraps the official `floci/floci:latest` Docker image. When the container starts it waits for port 4566 to be ready, then exposes:
|
||||
|
||||
| Method | Returns |
|
||||
|---|---|
|
||||
| `getEndpoint()` | `http://localhost:<mapped-port>` |
|
||||
| `getRegion()` | `us-east-1` (default) |
|
||||
| `getAccessKey()` | `test` |
|
||||
| `getSecretKey()` | `test` |
|
||||
|
||||
You pass these values directly into any AWS SDK client — no manual configuration, no environment variables.
|
||||
|
||||
## Language guides
|
||||
|
||||
- [Java](java.md) — JUnit 5, Spring Boot `@ServiceConnection`
|
||||
- [Node.js / TypeScript](nodejs.md) — Jest, Vitest
|
||||
- [Python](python.md) — pytest
|
||||
- [.NET](dotnet.md): xUnit
|
||||
- [Go](go.md) — in progress
|
||||
@@ -0,0 +1,232 @@
|
||||
# Testcontainers — Java
|
||||
|
||||
The `testcontainers-floci` library integrates Floci with [Testcontainers for Java](https://java.testcontainers.org/). It starts a real Floci container before your tests and shuts it down after, with no extra setup.
|
||||
|
||||
Two artifact lines are published to keep in sync with the Testcontainers major version:
|
||||
|
||||
| Testcontainers version | Spring Boot | Artifact version |
|
||||
|---|---|---|
|
||||
| 1.x | 3.x | `1.14.0` |
|
||||
| 2.x | 4.x | `2.15.0` |
|
||||
|
||||
## Installation
|
||||
|
||||
=== "Maven"
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.floci</groupId>
|
||||
<artifactId>testcontainers-floci</artifactId>
|
||||
<version>1.14.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
=== "Gradle"
|
||||
|
||||
```groovy
|
||||
testImplementation 'io.floci:testcontainers-floci:1.14.0'
|
||||
```
|
||||
|
||||
## Basic usage — JUnit 5
|
||||
|
||||
Annotate the class with `@Testcontainers` and declare a static `FlociContainer` field with `@Container`. Testcontainers handles the lifecycle automatically.
|
||||
|
||||
```java
|
||||
import io.floci.testcontainers.FlociContainer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Testcontainers
|
||||
class S3IntegrationTest {
|
||||
|
||||
@Container
|
||||
static FlociContainer floci = new FlociContainer();
|
||||
|
||||
@Test
|
||||
void shouldCreateBucket() {
|
||||
S3Client s3 = S3Client.builder()
|
||||
.endpointOverride(URI.create(floci.getEndpoint()))
|
||||
.region(Region.of(floci.getRegion()))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(floci.getAccessKey(), floci.getSecretKey())))
|
||||
.forcePathStyle(true)
|
||||
.build();
|
||||
|
||||
s3.createBucket(b -> b.bucket("my-bucket"));
|
||||
|
||||
assertThat(s3.listBuckets().buckets())
|
||||
.anyMatch(b -> b.name().equals("my-bucket"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SQS example
|
||||
|
||||
```java
|
||||
@Testcontainers
|
||||
class SqsIntegrationTest {
|
||||
|
||||
@Container
|
||||
static FlociContainer floci = new FlociContainer();
|
||||
|
||||
@Test
|
||||
void shouldSendAndReceiveMessage() {
|
||||
SqsClient sqs = SqsClient.builder()
|
||||
.endpointOverride(URI.create(floci.getEndpoint()))
|
||||
.region(Region.of(floci.getRegion()))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(floci.getAccessKey(), floci.getSecretKey())))
|
||||
.build();
|
||||
|
||||
String queueUrl = sqs.createQueue(b -> b.queueName("orders")).queueUrl();
|
||||
sqs.sendMessage(b -> b.queueUrl(queueUrl).messageBody("{\"event\":\"order.placed\"}"));
|
||||
|
||||
var messages = sqs.receiveMessage(b -> b.queueUrl(queueUrl).maxNumberOfMessages(1)).messages();
|
||||
assertThat(messages).hasSize(1);
|
||||
assertThat(messages.get(0).body()).contains("order.placed");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## DynamoDB example
|
||||
|
||||
```java
|
||||
@Testcontainers
|
||||
class DynamoDbIntegrationTest {
|
||||
|
||||
@Container
|
||||
static FlociContainer floci = new FlociContainer();
|
||||
|
||||
@Test
|
||||
void shouldCreateTableAndPutItem() {
|
||||
DynamoDbClient dynamo = DynamoDbClient.builder()
|
||||
.endpointOverride(URI.create(floci.getEndpoint()))
|
||||
.region(Region.of(floci.getRegion()))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(floci.getAccessKey(), floci.getSecretKey())))
|
||||
.build();
|
||||
|
||||
dynamo.createTable(b -> b
|
||||
.tableName("Orders")
|
||||
.attributeDefinitions(a -> a.attributeName("id").attributeType(ScalarAttributeType.S))
|
||||
.keySchema(k -> k.attributeName("id").keyType(KeyType.HASH))
|
||||
.billingMode(BillingMode.PAY_PER_REQUEST));
|
||||
|
||||
dynamo.putItem(b -> b
|
||||
.tableName("Orders")
|
||||
.item(Map.of("id", AttributeValue.fromS("order-1"),
|
||||
"status", AttributeValue.fromS("placed"))));
|
||||
|
||||
var item = dynamo.getItem(b -> b
|
||||
.tableName("Orders")
|
||||
.key(Map.of("id", AttributeValue.fromS("order-1")))).item();
|
||||
|
||||
assertThat(item.get("status").s()).isEqualTo("placed");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Spring Boot — `@ServiceConnection`
|
||||
|
||||
Add the Spring Boot companion artifact for zero-config auto-wiring. The `@ServiceConnection` annotation registers the container as a `ConnectionDetails` bean and configures all AWS SDK clients automatically.
|
||||
|
||||
=== "Maven"
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.floci</groupId>
|
||||
<artifactId>spring-boot-testcontainers-floci</artifactId>
|
||||
<version>1.14.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
=== "Gradle"
|
||||
|
||||
```groovy
|
||||
testImplementation 'io.floci:spring-boot-testcontainers-floci:1.14.0'
|
||||
```
|
||||
|
||||
```java
|
||||
import io.floci.testcontainers.FlociContainer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
class AppIntegrationTest {
|
||||
|
||||
@Container
|
||||
@ServiceConnection
|
||||
static FlociContainer floci = new FlociContainer();
|
||||
|
||||
@Autowired
|
||||
S3Client s3;
|
||||
|
||||
@Test
|
||||
void shouldCreateBucket() {
|
||||
s3.createBucket(b -> b.bucket("my-bucket"));
|
||||
|
||||
assertThat(s3.listBuckets().buckets())
|
||||
.anyMatch(b -> b.name().equals("my-bucket"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With `@ServiceConnection`, Spring Boot auto-configures the endpoint URL, region, and credentials for every AWS SDK client bean in the application context — no `application-test.yml` overrides needed.
|
||||
|
||||
## Reusing the container across tests
|
||||
|
||||
Declare the container in a shared base class or a JUnit 5 extension to start it once per test suite rather than once per class:
|
||||
|
||||
```java
|
||||
abstract class FlociTestBase {
|
||||
|
||||
@Container
|
||||
static FlociContainer floci = new FlociContainer();
|
||||
|
||||
static S3Client s3;
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClients() {
|
||||
s3 = S3Client.builder()
|
||||
.endpointOverride(URI.create(floci.getEndpoint()))
|
||||
.region(Region.of(floci.getRegion()))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(floci.getAccessKey(), floci.getSecretKey())))
|
||||
.forcePathStyle(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Testcontainers
|
||||
class MyServiceTest extends FlociTestBase {
|
||||
|
||||
@Test
|
||||
void myTest() {
|
||||
s3.createBucket(b -> b.bucket("test-bucket"));
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Source and changelog
|
||||
|
||||
[github.com/floci-io/testcontainers-floci](https://github.com/floci-io/testcontainers-floci)
|
||||
@@ -0,0 +1,270 @@
|
||||
# Testcontainers — Node.js / TypeScript
|
||||
|
||||
The `@floci/testcontainers` package integrates Floci with [Testcontainers for Node.js](https://node.testcontainers.org/). It works with any test runner that supports `async`/`await` — Jest, Vitest, Mocha, and others.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm install --save-dev @floci/testcontainers
|
||||
```
|
||||
|
||||
```sh
|
||||
# yarn
|
||||
yarn add --dev @floci/testcontainers
|
||||
|
||||
# pnpm
|
||||
pnpm add -D @floci/testcontainers
|
||||
```
|
||||
|
||||
## Basic usage — Jest
|
||||
|
||||
```typescript
|
||||
import { FlociContainer } from "@floci/testcontainers";
|
||||
import { S3Client, CreateBucketCommand, ListBucketsCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
describe("S3", () => {
|
||||
let floci: FlociContainer;
|
||||
|
||||
beforeAll(async () => {
|
||||
floci = await new FlociContainer().start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await floci.stop();
|
||||
});
|
||||
|
||||
it("should create and list a bucket", async () => {
|
||||
const s3 = new S3Client({
|
||||
endpoint: floci.getEndpoint(),
|
||||
region: floci.getRegion(),
|
||||
credentials: {
|
||||
accessKeyId: floci.getAccessKey(),
|
||||
secretAccessKey: floci.getSecretKey(),
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
await s3.send(new CreateBucketCommand({ Bucket: "my-bucket" }));
|
||||
|
||||
const { Buckets } = await s3.send(new ListBucketsCommand({}));
|
||||
expect(Buckets?.some(b => b.Name === "my-bucket")).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## SQS example
|
||||
|
||||
```typescript
|
||||
import { FlociContainer } from "@floci/testcontainers";
|
||||
import {
|
||||
SQSClient,
|
||||
CreateQueueCommand,
|
||||
SendMessageCommand,
|
||||
ReceiveMessageCommand,
|
||||
} from "@aws-sdk/client-sqs";
|
||||
|
||||
describe("SQS", () => {
|
||||
let floci: FlociContainer;
|
||||
let sqs: SQSClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
floci = await new FlociContainer().start();
|
||||
sqs = new SQSClient({
|
||||
endpoint: floci.getEndpoint(),
|
||||
region: floci.getRegion(),
|
||||
credentials: {
|
||||
accessKeyId: floci.getAccessKey(),
|
||||
secretAccessKey: floci.getSecretKey(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await floci.stop();
|
||||
});
|
||||
|
||||
it("should send and receive a message", async () => {
|
||||
const { QueueUrl } = await sqs.send(
|
||||
new CreateQueueCommand({ QueueName: "orders" })
|
||||
);
|
||||
|
||||
await sqs.send(
|
||||
new SendMessageCommand({
|
||||
QueueUrl,
|
||||
MessageBody: JSON.stringify({ event: "order.placed" }),
|
||||
})
|
||||
);
|
||||
|
||||
const { Messages } = await sqs.send(
|
||||
new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 1 })
|
||||
);
|
||||
|
||||
expect(Messages).toHaveLength(1);
|
||||
expect(JSON.parse(Messages![0].Body!).event).toBe("order.placed");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## DynamoDB example
|
||||
|
||||
```typescript
|
||||
import { FlociContainer } from "@floci/testcontainers";
|
||||
import {
|
||||
DynamoDBClient,
|
||||
CreateTableCommand,
|
||||
PutItemCommand,
|
||||
GetItemCommand,
|
||||
} from "@aws-sdk/client-dynamodb";
|
||||
|
||||
describe("DynamoDB", () => {
|
||||
let floci: FlociContainer;
|
||||
let dynamo: DynamoDBClient;
|
||||
|
||||
beforeAll(async () => {
|
||||
floci = await new FlociContainer().start();
|
||||
dynamo = new DynamoDBClient({
|
||||
endpoint: floci.getEndpoint(),
|
||||
region: floci.getRegion(),
|
||||
credentials: {
|
||||
accessKeyId: floci.getAccessKey(),
|
||||
secretAccessKey: floci.getSecretKey(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await floci.stop();
|
||||
});
|
||||
|
||||
it("should put and get an item", async () => {
|
||||
await dynamo.send(
|
||||
new CreateTableCommand({
|
||||
TableName: "Orders",
|
||||
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
|
||||
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
||||
BillingMode: "PAY_PER_REQUEST",
|
||||
})
|
||||
);
|
||||
|
||||
await dynamo.send(
|
||||
new PutItemCommand({
|
||||
TableName: "Orders",
|
||||
Item: {
|
||||
id: { S: "order-1" },
|
||||
status: { S: "placed" },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const { Item } = await dynamo.send(
|
||||
new GetItemCommand({
|
||||
TableName: "Orders",
|
||||
Key: { id: { S: "order-1" } },
|
||||
})
|
||||
);
|
||||
|
||||
expect(Item?.status?.S).toBe("placed");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Vitest
|
||||
|
||||
The same pattern works with Vitest — replace `describe`/`it`/`expect` with their Vitest equivalents (the API is identical):
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { FlociContainer } from "@floci/testcontainers";
|
||||
import { S3Client, CreateBucketCommand, ListBucketsCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
describe("S3", () => {
|
||||
let floci: FlociContainer;
|
||||
|
||||
beforeAll(async () => {
|
||||
floci = await new FlociContainer().start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await floci.stop();
|
||||
});
|
||||
|
||||
it("should create a bucket", async () => {
|
||||
const s3 = new S3Client({
|
||||
endpoint: floci.getEndpoint(),
|
||||
region: floci.getRegion(),
|
||||
credentials: {
|
||||
accessKeyId: floci.getAccessKey(),
|
||||
secretAccessKey: floci.getSecretKey(),
|
||||
},
|
||||
forcePathStyle: true,
|
||||
});
|
||||
|
||||
await s3.send(new CreateBucketCommand({ Bucket: "vitest-bucket" }));
|
||||
|
||||
const { Buckets } = await s3.send(new ListBucketsCommand({}));
|
||||
expect(Buckets?.some(b => b.Name === "vitest-bucket")).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Reusing the container across test files
|
||||
|
||||
Start the container once in a global setup file and expose the endpoint via an environment variable or a shared module so individual test files don't each start their own container.
|
||||
|
||||
=== "Jest — globalSetup"
|
||||
|
||||
```typescript
|
||||
// jest.global-setup.ts
|
||||
import { FlociContainer } from "@floci/testcontainers";
|
||||
|
||||
let floci: FlociContainer;
|
||||
|
||||
export async function setup() {
|
||||
floci = await new FlociContainer().start();
|
||||
process.env.FLOCI_ENDPOINT = floci.getEndpoint();
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
await floci?.stop();
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// jest.config.json
|
||||
{
|
||||
"globalSetup": "./jest.global-setup.ts"
|
||||
}
|
||||
```
|
||||
|
||||
=== "Vitest — globalSetup"
|
||||
|
||||
```typescript
|
||||
// vitest.global-setup.ts
|
||||
import { FlociContainer } from "@floci/testcontainers";
|
||||
|
||||
let floci: FlociContainer;
|
||||
|
||||
export async function setup() {
|
||||
floci = await new FlociContainer().start();
|
||||
process.env.FLOCI_ENDPOINT = floci.getEndpoint();
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
await floci?.stop();
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globalSetup: "./vitest.global-setup.ts",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Source and changelog
|
||||
|
||||
[github.com/floci-io/testcontainers-floci-node](https://github.com/floci-io/testcontainers-floci-node)
|
||||
@@ -0,0 +1,229 @@
|
||||
# Testcontainers — Python
|
||||
|
||||
The `testcontainers-floci` package integrates Floci with [Testcontainers for Python](https://testcontainers-python.readthedocs.io/). It works as a context manager and integrates naturally with pytest fixtures.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pip install testcontainers-floci
|
||||
```
|
||||
|
||||
```sh
|
||||
# poetry
|
||||
poetry add --group dev testcontainers-floci
|
||||
|
||||
# uv
|
||||
uv add --dev testcontainers-floci
|
||||
```
|
||||
|
||||
## Basic usage — context manager
|
||||
|
||||
```python
|
||||
import boto3
|
||||
from floci import FlociContainer
|
||||
|
||||
|
||||
def test_s3_create_bucket():
|
||||
with FlociContainer() as floci:
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url=floci.get_endpoint(),
|
||||
region_name=floci.get_region(),
|
||||
aws_access_key_id=floci.get_access_key(),
|
||||
aws_secret_access_key=floci.get_secret_key(),
|
||||
)
|
||||
|
||||
s3.create_bucket(Bucket="my-bucket")
|
||||
|
||||
buckets = [b["Name"] for b in s3.list_buckets()["Buckets"]]
|
||||
assert "my-bucket" in buckets
|
||||
```
|
||||
|
||||
## Pytest fixture
|
||||
|
||||
Use a session-scoped fixture so the container starts once and is shared across all tests in the suite.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import boto3
|
||||
from floci import FlociContainer
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def floci():
|
||||
with FlociContainer() as container:
|
||||
yield container
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def s3_client(floci):
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=floci.get_endpoint(),
|
||||
region_name=floci.get_region(),
|
||||
aws_access_key_id=floci.get_access_key(),
|
||||
aws_secret_access_key=floci.get_secret_key(),
|
||||
)
|
||||
|
||||
|
||||
def test_create_bucket(s3_client):
|
||||
s3_client.create_bucket(Bucket="my-bucket")
|
||||
buckets = [b["Name"] for b in s3_client.list_buckets()["Buckets"]]
|
||||
assert "my-bucket" in buckets
|
||||
|
||||
|
||||
def test_upload_object(s3_client):
|
||||
s3_client.create_bucket(Bucket="uploads")
|
||||
s3_client.put_object(Bucket="uploads", Key="hello.txt", Body=b"hello floci")
|
||||
body = s3_client.get_object(Bucket="uploads", Key="hello.txt")["Body"].read()
|
||||
assert body == b"hello floci"
|
||||
```
|
||||
|
||||
## SQS example
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import boto3
|
||||
import json
|
||||
from floci import FlociContainer
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def floci():
|
||||
with FlociContainer() as container:
|
||||
yield container
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sqs_client(floci):
|
||||
return boto3.client(
|
||||
"sqs",
|
||||
endpoint_url=floci.get_endpoint(),
|
||||
region_name=floci.get_region(),
|
||||
aws_access_key_id=floci.get_access_key(),
|
||||
aws_secret_access_key=floci.get_secret_key(),
|
||||
)
|
||||
|
||||
|
||||
def test_send_and_receive_message(sqs_client):
|
||||
queue_url = sqs_client.create_queue(QueueName="orders")["QueueUrl"]
|
||||
|
||||
sqs_client.send_message(
|
||||
QueueUrl=queue_url,
|
||||
MessageBody=json.dumps({"event": "order.placed"}),
|
||||
)
|
||||
|
||||
response = sqs_client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=1)
|
||||
messages = response.get("Messages", [])
|
||||
|
||||
assert len(messages) == 1
|
||||
assert json.loads(messages[0]["Body"])["event"] == "order.placed"
|
||||
```
|
||||
|
||||
## DynamoDB example
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import boto3
|
||||
from floci import FlociContainer
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def floci():
|
||||
with FlociContainer() as container:
|
||||
yield container
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dynamo_client(floci):
|
||||
return boto3.client(
|
||||
"dynamodb",
|
||||
endpoint_url=floci.get_endpoint(),
|
||||
region_name=floci.get_region(),
|
||||
aws_access_key_id=floci.get_access_key(),
|
||||
aws_secret_access_key=floci.get_secret_key(),
|
||||
)
|
||||
|
||||
|
||||
def test_put_and_get_item(dynamo_client):
|
||||
dynamo_client.create_table(
|
||||
TableName="Orders",
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
|
||||
dynamo_client.put_item(
|
||||
TableName="Orders",
|
||||
Item={"id": {"S": "order-1"}, "status": {"S": "placed"}},
|
||||
)
|
||||
|
||||
item = dynamo_client.get_item(
|
||||
TableName="Orders",
|
||||
Key={"id": {"S": "order-1"}},
|
||||
)["Item"]
|
||||
|
||||
assert item["status"]["S"] == "placed"
|
||||
```
|
||||
|
||||
## Secrets Manager example
|
||||
|
||||
```python
|
||||
def test_create_and_get_secret(floci):
|
||||
sm = boto3.client(
|
||||
"secretsmanager",
|
||||
endpoint_url=floci.get_endpoint(),
|
||||
region_name=floci.get_region(),
|
||||
aws_access_key_id=floci.get_access_key(),
|
||||
aws_secret_access_key=floci.get_secret_key(),
|
||||
)
|
||||
|
||||
sm.create_secret(Name="db/password", SecretString="supersecret")
|
||||
value = sm.get_secret_value(SecretId="db/password")["SecretString"]
|
||||
assert value == "supersecret"
|
||||
```
|
||||
|
||||
## conftest.py pattern
|
||||
|
||||
Place shared fixtures in `conftest.py` so every test module picks them up automatically:
|
||||
|
||||
```python
|
||||
# conftest.py
|
||||
import pytest
|
||||
import boto3
|
||||
from floci import FlociContainer
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def floci():
|
||||
with FlociContainer() as container:
|
||||
yield container
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def aws_clients(floci):
|
||||
kwargs = dict(
|
||||
endpoint_url=floci.get_endpoint(),
|
||||
region_name=floci.get_region(),
|
||||
aws_access_key_id=floci.get_access_key(),
|
||||
aws_secret_access_key=floci.get_secret_key(),
|
||||
)
|
||||
return {
|
||||
"s3": boto3.client("s3", **kwargs),
|
||||
"sqs": boto3.client("sqs", **kwargs),
|
||||
"dynamodb": boto3.client("dynamodb", **kwargs),
|
||||
"secretsmanager": boto3.client("secretsmanager", **kwargs),
|
||||
}
|
||||
```
|
||||
|
||||
```python
|
||||
# test_my_service.py
|
||||
def test_something(aws_clients):
|
||||
s3 = aws_clients["s3"]
|
||||
s3.create_bucket(Bucket="test")
|
||||
# ...
|
||||
```
|
||||
|
||||
## Source and changelog
|
||||
|
||||
[github.com/floci-io/testcontainers-floci-python](https://github.com/floci-io/testcontainers-floci-python)
|
||||
Reference in New Issue
Block a user