Skip to content

Commit 2c4117c

Browse files
VirtueMeclaude
andauthored
feat(test): add integration test suite with LocalStack (#745)
Deploys 9 CloudFormation stacks against LocalStack on every PR to catch issues that unit tests cannot — circular resource dependencies, invalid ARN references, and misconfigured IAM policies. Fixtures cover: standard workflow (with tags), express workflow (with loggingConfig and tracingConfig), activities, CloudWatch alarms, notifications (SNS/SQS), schedule events (rate rule and EventBridge Scheduler), CloudWatch events, API Gateway, and noOutput. All fixtures share fixtures/package.json so CI requires only two npm install calls. A base.yml holds shared provider/plugin/package config. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 60e195c commit 2c4117c

File tree

25 files changed

+14119
-3680
lines changed

25 files changed

+14119
-3680
lines changed

.github/workflows/integration.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Integration Tests
2+
3+
on:
4+
pull_request:
5+
paths-ignore:
6+
- "**/*.md"
7+
8+
jobs:
9+
integration:
10+
runs-on: ubuntu-latest
11+
timeout-minutes: 15
12+
steps:
13+
- uses: actions/checkout@v5
14+
- uses: actions/setup-node@v6
15+
with:
16+
node-version: 22
17+
cache: npm
18+
- name: Start LocalStack
19+
run: docker compose up -d
20+
- name: Install dependencies
21+
run: |
22+
npm install --legacy-peer-deps
23+
npm install --legacy-peer-deps --prefix fixtures/
24+
- name: Wait for LocalStack
25+
run: |
26+
echo "Waiting for LocalStack to be ready..."
27+
for i in $(seq 1 30); do
28+
HEALTH=$(curl -s http://localhost:4566/_localstack/health)
29+
if echo "$HEALTH" | grep -q '"cloudformation": "available"' && \
30+
echo "$HEALTH" | grep -q '"s3": "available"' && \
31+
echo "$HEALTH" | grep -q '"stepfunctions": "available"'; then
32+
echo "LocalStack is ready"
33+
exit 0
34+
fi
35+
sleep 2
36+
done
37+
echo "LocalStack did not become ready in time"
38+
exit 1
39+
- name: Deploy fixtures
40+
run: npm run test:integration

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ admin.env
4040
.env
4141
tmp
4242
.coveralls.yml
43-
tmpdirs-serverless
43+
tmpdirs-serverless
44+
.serverless

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fixtures/
2+
.worktrees/

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,56 @@ stepFunctions:
15791579

15801580
As a result, `hellostepfunc1` will only have the tag of `score: 42`, and _not_ the tags at the provider level
15811581

1582+
## Integration Tests
1583+
1584+
Integration tests deploy real CloudFormation stacks to [LocalStack](https://localstack.cloud) to catch issues that unit tests cannot (e.g. circular resource dependencies, invalid ARN references).
1585+
1586+
### Prerequisites
1587+
1588+
- Docker
1589+
1590+
### Running all fixtures
1591+
1592+
Install dependencies, start LocalStack, then run the integration test script:
1593+
1594+
```bash
1595+
npm install --legacy-peer-deps
1596+
npm install --legacy-peer-deps --prefix fixtures/
1597+
docker compose up -d
1598+
npm run test:integration
1599+
```
1600+
1601+
### Running a single fixture
1602+
1603+
Use the `fixture:command` syntax directly with `osls`:
1604+
1605+
```bash
1606+
osls basic-state-machine:deploy --stage test
1607+
```
1608+
1609+
### Adding a fixture
1610+
1611+
Create a new directory under `fixtures/` containing a `serverless.yml`. It will be picked up automatically by `fixtures/serverless-compose.js`. No `package.json` is needed — all fixtures share `fixtures/package.json`.
1612+
1613+
Use `fixtures/base.yml` for all shared configuration (`provider`, `plugins`, `package`, `custom`):
1614+
1615+
```yaml
1616+
service: integration-my-fixture
1617+
1618+
provider: ${file(../base.yml):provider}
1619+
plugins: ${file(../base.yml):plugins}
1620+
package: ${file(../base.yml):package}
1621+
custom: ${file(../base.yml):custom}
1622+
1623+
stepFunctions:
1624+
stateMachines:
1625+
...
1626+
```
1627+
1628+
The `package` section in `base.yml` sets `excludeDevDependencies: false`, which is required to prevent OOM during deployment. Serverless Framework v3 defaults this to `true`, causing it to scan all files in `node_modules` (~25k files per fixture) to identify production dependencies before packaging — that analysis exhausts the JS heap.
1629+
1630+
> **Note:** `serverless-compose.js` at the repo root is intentionally a one-line re-export of `fixtures/serverless-compose.js`. It must exist at the root because `@osls/compose` resolves the config via `lstat`, which does not follow symlinks. Do not delete it.
1631+
15821632
## Commands
15831633

15841634
### deploy

docker-compose.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
localstack:
3+
image: localstack/localstack:4
4+
network_mode: host
5+
environment:
6+
SERVICES: cloudformation,stepfunctions,iam,lambda,s3,logs,sns,cloudwatch,sqs,events,scheduler,apigateway
7+
volumes:
8+
- /var/run/docker.sock:/var/run/docker.sock

fixtures/activities/serverless.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
service: integration-activities
2+
3+
provider: ${file(../base.yml):provider}
4+
plugins: ${file(../base.yml):plugins}
5+
package: ${file(../base.yml):package}
6+
custom: ${file(../base.yml):custom}
7+
8+
stepFunctions:
9+
stateMachines:
10+
activityMachine:
11+
name: integration-activities-${opt:stage, 'test'}
12+
definition:
13+
StartAt: WaitForActivity
14+
States:
15+
WaitForActivity:
16+
Type: Task
17+
Resource:
18+
Ref: MyActivityTaskStepFunctionsActivity
19+
End: true
20+
activities:
21+
- MyActivityTask
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
service: integration-api-gateway
2+
3+
provider: ${file(../base.yml):provider}
4+
plugins: ${file(../base.yml):plugins}
5+
package: ${file(../base.yml):package}
6+
custom: ${file(../base.yml):custom}
7+
8+
stepFunctions:
9+
stateMachines:
10+
apiMachine:
11+
name: integration-api-gateway-${opt:stage, 'test'}
12+
events:
13+
- http:
14+
path: execute
15+
method: POST
16+
definition:
17+
StartAt: PassThrough
18+
States:
19+
PassThrough:
20+
Type: Pass
21+
End: true

fixtures/base.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
provider:
2+
name: aws
3+
runtime: nodejs22.x
4+
region: us-east-1
5+
stage: test
6+
7+
plugins:
8+
- serverless-localstack
9+
- serverless-step-functions
10+
11+
# excludeDevDependencies must be false to prevent Serverless Framework v3 from
12+
# scanning node_modules (~25k files) to identify production dependencies before
13+
# packaging — that analysis exhausts the JS heap. The !node_modules/** pattern
14+
# then excludes node_modules from the deployment package.
15+
package:
16+
excludeDevDependencies: false
17+
patterns:
18+
- '!node_modules/**'
19+
20+
custom:
21+
localstack:
22+
stages:
23+
- test
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports.hello = async () => ({ statusCode: 200 });
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
service: integration-basic-state-machine
2+
3+
provider: ${file(../base.yml):provider}
4+
plugins: ${file(../base.yml):plugins}
5+
package: ${file(../base.yml):package}
6+
custom: ${file(../base.yml):custom}
7+
8+
functions:
9+
hello:
10+
handler: handler.hello
11+
12+
stepFunctions:
13+
stateMachines:
14+
basicMachine:
15+
name: integration-basic-${opt:stage, 'test'}
16+
tags:
17+
integration: 'true'
18+
fixture: basic-state-machine
19+
definition:
20+
StartAt: InvokeHello
21+
States:
22+
InvokeHello:
23+
Type: Task
24+
Resource:
25+
Fn::GetAtt: [HelloLambdaFunction, Arn]
26+
End: true

0 commit comments

Comments
 (0)