Skip to content

Commit 3169aed

Browse files
committed
Updated the vote service tests
1 parent 18a9f29 commit 3169aed

File tree

5 files changed

+236
-32
lines changed

5 files changed

+236
-32
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Build and Push Images
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
env:
10+
REGISTRY: ghcr.io
11+
VOTE_IMAGE_NAME: ${{ github.repository }}/vote
12+
RESULT_IMAGE_NAME: ${{ github.repository }}/result
13+
WORKER_IMAGE_NAME: ${{ github.repository }}/worker
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
packages: write
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Log in to the Container registry
27+
uses: docker/login-action@v3
28+
with:
29+
registry: ${{ env.REGISTRY }}
30+
username: ${{ github.actor }}
31+
password: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Set up Docker Buildx
34+
uses: docker/setup-buildx-action@v3
35+
36+
- name: Build and push Vote image
37+
uses: docker/build-push-action@v5
38+
with:
39+
context: ./vote
40+
push: ${{ github.event_name != 'pull_request' }}
41+
tags: ${{ env.REGISTRY }}/${{ env.VOTE_IMAGE_NAME }}:${{ github.sha }},${{ env.REGISTRY }}/${{ env.VOTE_IMAGE_NAME }}:latest
42+
cache-from: type=gha
43+
cache-to: type=gha,mode=max
44+
45+
- name: Build and push Result image
46+
uses: docker/build-push-action@v5
47+
with:
48+
context: ./result
49+
push: ${{ github.event_name != 'pull_request' }}
50+
tags: ${{ env.REGISTRY }}/${{ env.RESULT_IMAGE_NAME }}:${{ github.sha }},${{ env.REGISTRY }}/${{ env.RESULT_IMAGE_NAME }}:latest
51+
cache-from: type=gha
52+
cache-to: type=gha,mode=max
53+
54+
- name: Build and push Worker image
55+
uses: docker/build-push-action@v5
56+
with:
57+
context: ./worker
58+
push: ${{ github.event_name != 'pull_request' }}
59+
tags: ${{ env.REGISTRY }}/${{ env.WORKER_IMAGE_NAME }}:${{ github.sha }},${{ env.REGISTRY }}/${{ env.WORKER_IMAGE_NAME }}:latest
60+
cache-from: type=gha
61+
cache-to: type=gha,mode=max
62+
63+
- name: Test the application
64+
run: |
65+
docker compose up -d
66+
sleep 30 # Give some time for services to start
67+
68+
# Basic health checks
69+
curl -f http://localhost:8080 || exit 1 # Vote app
70+
curl -f http://localhost:8081 || exit 1 # Result app
71+
72+
docker compose down

.github/workflows/build.yml

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,64 @@
1+
# Primary workflow for building and testing the voting application
2+
# This workflow uses standard GitHub-hosted runners and Docker BuildKit
3+
14
name: Build and Push Images
25

6+
# Trigger the workflow on push to main branch or when a PR is opened/updated
37
on:
48
push:
59
branches: [ "main" ]
610
pull_request:
711
branches: [ "main" ]
812

13+
# Global environment variables used throughout the workflow
914
env:
15+
# Define the container registry and image names
1016
REGISTRY: ghcr.io
1117
VOTE_IMAGE_NAME: ${{ github.repository }}/vote
1218
RESULT_IMAGE_NAME: ${{ github.repository }}/result
1319
WORKER_IMAGE_NAME: ${{ github.repository }}/worker
1420

1521
jobs:
1622
build:
23+
# Use latest Ubuntu runner provided by GitHub Actions
1724
runs-on: ubuntu-latest
25+
26+
# Define required permissions for the job
1827
permissions:
19-
contents: read
20-
packages: write
28+
contents: read # Required to checkout code
29+
packages: write # Required to push to GitHub Packages
2130

2231
steps:
32+
# Step 1: Check out the repository code
2333
- name: Checkout repository
2434
uses: actions/checkout@v4
35+
# This step clones the repository and sets up the workspace
2536

37+
# Step 2: Set up Docker Buildx for multi-platform builds
38+
- name: Set up Docker Buildx
39+
uses: docker/setup-buildx-action@v3
40+
# Buildx provides enhanced build capabilities including multi-arch builds
41+
42+
# Step 3: Authenticate with GitHub Container Registry
2643
- name: Log in to the Container registry
2744
uses: docker/login-action@v3
2845
with:
2946
registry: ${{ env.REGISTRY }}
3047
username: ${{ github.actor }}
3148
password: ${{ secrets.GITHUB_TOKEN }}
49+
# This allows us to push images to GitHub Container Registry
3250

33-
- name: Set up Docker Buildx
34-
uses: docker/setup-buildx-action@v3
35-
51+
# Step 4: Build and push the Vote service image
3652
- name: Build and push Vote image
3753
uses: docker/build-push-action@v5
3854
with:
3955
context: ./vote
40-
push: ${{ github.event_name != 'pull_request' }}
56+
push: ${{ github.event_name != 'pull_request' }} # Only push on main branch
4157
tags: ${{ env.REGISTRY }}/${{ env.VOTE_IMAGE_NAME }}:${{ github.sha }},${{ env.REGISTRY }}/${{ env.VOTE_IMAGE_NAME }}:latest
42-
cache-from: type=gha
58+
cache-from: type=gha # Use GitHub Actions cache
4359
cache-to: type=gha,mode=max
4460

61+
# Step 5: Build and push the Result service image
4562
- name: Build and push Result image
4663
uses: docker/build-push-action@v5
4764
with:
@@ -51,6 +68,7 @@ jobs:
5168
cache-from: type=gha
5269
cache-to: type=gha,mode=max
5370

71+
# Step 6: Build and push the Worker service image
5472
- name: Build and push Worker image
5573
uses: docker/build-push-action@v5
5674
with:
@@ -60,13 +78,18 @@ jobs:
6078
cache-from: type=gha
6179
cache-to: type=gha,mode=max
6280

81+
# Step 7: Test the application
6382
- name: Test the application
6483
run: |
84+
# Start all services using docker-compose
6585
docker compose up -d
66-
sleep 30 # Give some time for services to start
6786
68-
# Basic health checks
87+
# Wait for services to initialize
88+
sleep 30
89+
90+
# Perform health checks on the services
6991
curl -f http://localhost:8080 || exit 1 # Vote app
7092
curl -f http://localhost:8081 || exit 1 # Result app
7193
94+
# Clean up containers
7295
docker compose down
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Integration Test Workflow for the Vote Service
2+
# This workflow runs testcontainer-based integration tests for the voting application
3+
4+
name: Integration Tests
5+
6+
# Only trigger on changes to the vote service
7+
on:
8+
push:
9+
branches: [ "main" ]
10+
paths:
11+
- 'vote/**'
12+
pull_request:
13+
branches: [ "main" ]
14+
paths:
15+
- 'vote/**'
16+
17+
jobs:
18+
test:
19+
# Use latest Ubuntu runner provided by GitHub Actions
20+
runs-on: ubuntu-latest
21+
22+
# Set up required services for testing
23+
services:
24+
# Docker-in-Docker service required for testcontainers
25+
dind:
26+
image: docker:dind
27+
ports:
28+
- 2375:2375 # Expose Docker daemon port
29+
env:
30+
DOCKER_TLS_CERTDIR: "" # Disable TLS for local development
31+
options: >-
32+
--privileged
33+
--health-cmd "docker info"
34+
--health-interval 10s
35+
--health-timeout 5s
36+
--health-retries 5
37+
38+
steps:
39+
# Step 1: Check out the repository code
40+
- uses: actions/checkout@v4
41+
# This provides access to the repository content
42+
43+
# Step 2: Set up Python environment
44+
- name: Set up Python
45+
uses: actions/setup-python@v4
46+
with:
47+
python-version: '3.11' # Use Python 3.11 for testing
48+
cache: 'pip' # Enable pip caching for faster installations
49+
50+
# Step 3: Install test dependencies
51+
- name: Install dependencies
52+
working-directory: ./vote
53+
run: |
54+
python -m pip install --upgrade pip
55+
pip install -r requirements-test.txt
56+
# Install all required packages for running tests
57+
58+
# Step 4: Run integration tests
59+
- name: Run tests
60+
working-directory: ./vote
61+
env:
62+
# Configure Docker host for testcontainers
63+
DOCKER_HOST: tcp://localhost:2375
64+
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: /var/run/docker.sock
65+
# Add vote directory to Python path
66+
PYTHONPATH: ${{ github.workspace }}/vote
67+
run: |
68+
# Run pytest with verbose output
69+
pytest test_vote_service.py -v

vote/requirements-test.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ pytest==7.4.3
22
testcontainers==3.7.1
33
requests==2.31.0
44
redis==5.0.1
5+
Flask==3.0.0
6+
gunicorn==21.2.0
7+
watchdog==3.0.0

vote/test_vote_service.py

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,51 @@
22
import requests
33
from testcontainers.redis import RedisContainer
44
from testcontainers.core.container import DockerContainer
5+
from testcontainers.core.waiting_utils import wait_for_logs
56
import json
67
import time
78

89
class VoteAppContainer(DockerContainer):
9-
def __init__(self, redis_host, redis_port):
10-
super(VoteAppContainer, self).__init__("example-voting-app-vote")
11-
self.with_env("REDIS_HOST", redis_host)
12-
self.with_env("REDIS_PORT", str(redis_port))
10+
"""Custom container class for the voting application"""
11+
12+
def __init__(self):
13+
# Use the same image name as in docker-compose
14+
super(VoteAppContainer, self).__init__("example-voting-app-vote:test")
1315
self.with_exposed_ports(80)
16+
self.with_env("FLASK_ENV", "development")
17+
18+
def start(self):
19+
"""Override start to wait for the application to be ready"""
20+
super().start()
21+
# Wait for Gunicorn to start
22+
wait_for_logs(self, "Listening at: http://0.0.0.0:80")
23+
return self
1424

1525
@pytest.fixture(scope="function")
1626
def vote_app_environment():
27+
"""Fixture that provides a complete voting app environment with Redis"""
28+
1729
# Start Redis container
1830
with RedisContainer() as redis:
31+
redis_host = redis.get_container_host_ip()
32+
redis_port = redis.get_exposed_port(6379)
33+
1934
# Build the vote app image
2035
import subprocess
21-
subprocess.run(["docker", "build", "-t", "example-voting-app-vote", "."], check=True)
36+
subprocess.run([
37+
"docker", "build",
38+
"-t", "example-voting-app-vote:test",
39+
"-f", "Dockerfile",
40+
"--target", "dev", # Use dev target for faster builds
41+
"."
42+
], check=True)
2243

2344
# Start vote app container
24-
with VoteAppContainer(
25-
redis_host=redis.get_container_host_ip(),
26-
redis_port=redis.get_exposed_port(6379)
27-
) as vote_app:
28-
# Wait for the application to be ready
29-
time.sleep(2)
45+
with VoteAppContainer() as vote_app:
46+
# Configure Redis connection
47+
vote_app.with_env("REDIS_HOST", redis_host)
48+
vote_app.with_env("REDIS_PORT", str(redis_port))
49+
3050
vote_app_url = f"http://{vote_app.get_container_host_ip()}:{vote_app.get_exposed_port(80)}"
3151

3252
yield {
@@ -35,41 +55,58 @@ def vote_app_environment():
3555
}
3656

3757
def test_vote_submission(vote_app_environment):
38-
# Get the URL where the vote app is running
58+
"""Test that votes can be submitted and are stored in Redis"""
59+
3960
vote_app_url = vote_app_environment["vote_app_url"]
4061

4162
# First request to get the cookie
4263
response = requests.get(vote_app_url)
43-
assert response.status_code == 200
64+
assert response.status_code == 200, "Failed to get initial page"
4465
cookies = response.cookies
4566

4667
# Submit a vote
4768
vote_data = {"vote": "a"}
4869
response = requests.post(vote_app_url, data=vote_data, cookies=cookies)
49-
assert response.status_code == 200
70+
assert response.status_code == 200, "Failed to submit vote"
5071

5172
# Verify vote was stored in Redis
5273
redis = vote_app_environment["redis"]
5374
redis_client = redis.get_client()
54-
votes = redis_client.lrange("votes", 0, -1)
5575

56-
# There should be at least one vote
57-
assert len(votes) > 0
76+
# Wait for the vote to be processed (add retry logic)
77+
max_retries = 5
78+
for _ in range(max_retries):
79+
votes = redis_client.lrange("votes", 0, -1)
80+
if votes:
81+
break
82+
time.sleep(1)
83+
else:
84+
pytest.fail("Vote was not stored in Redis after multiple retries")
5885

5986
# Parse the latest vote
6087
latest_vote = json.loads(votes[-1])
61-
assert "vote" in latest_vote
62-
assert latest_vote["vote"] == "a"
88+
assert "vote" in latest_vote, "Vote data is missing vote field"
89+
assert latest_vote["vote"] == "a", f"Expected vote 'a' but got {latest_vote['vote']}"
6390

6491
def test_options_displayed(vote_app_environment):
65-
# Get the URL where the vote app is running
92+
"""Test that voting options are correctly displayed"""
93+
6694
vote_app_url = vote_app_environment["vote_app_url"]
6795

6896
# Get the main page
6997
response = requests.get(vote_app_url)
70-
assert response.status_code == 200
98+
assert response.status_code == 200, "Failed to get voting page"
7199

72100
# Check if both options are in the response
73101
content = response.text
74-
assert "Cats" in content
75-
assert "Dogs" in content
102+
assert "Cats" in content, "Option 'Cats' not found in page"
103+
assert "Dogs" in content, "Option 'Dogs' not found in page"
104+
105+
def test_health_check(vote_app_environment):
106+
"""Test the application's health check endpoint"""
107+
108+
vote_app_url = vote_app_environment["vote_app_url"]
109+
110+
# Make a request to the root endpoint
111+
response = requests.get(vote_app_url)
112+
assert response.status_code == 200, "Health check failed"

0 commit comments

Comments
 (0)