Skip to content

Commit

Permalink
Merge pull request #2 from weesvc/add-testcontainers
Browse files Browse the repository at this point in the history
Add testcontainers
  • Loading branch information
javaducky authored Mar 11, 2024
2 parents c035f31 + a4ae31a commit 7081cbc
Show file tree
Hide file tree
Showing 11 changed files with 387 additions and 28 deletions.
41 changes: 35 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,40 @@ on:
pull_request:

jobs:
test-and-compliance:
# TODO Tests aren't working with Github Actions for some reason, but run fine locally
# run-unit-tests:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# - name: Install Java
# uses: actions/setup-java@v4
# with:
# distribution: 'temurin'
# java-version: '21'
# cache: 'gradle'
# - name: Setup Gradle
# uses: gradle/actions/setup-gradle@v3
# - name: Run unit tests
# run: |
# ./gradlew test

check-compliance:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15.3-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: defaultdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand All @@ -22,14 +54,11 @@ jobs:
cache: 'gradle'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Run unit tests
run: |
./gradlew test
- name: Install k6
run: |
curl https://github.com/grafana/k6/releases/download/v0.48.0/k6-v0.48.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1
curl https://github.com/grafana/k6/releases/download/v0.49.0/k6-v0.49.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1
- name: k6 Compliance
run: |
./gradlew bootRun &
sleep 3s
sleep 30s
./k6 run -e PORT=8080 https://raw.githubusercontent.com/weesvc/workbench/main/scripts/api-compliance.js
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,19 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter'
implementation platform('org.testcontainers:testcontainers-bom:1.19.7')

testImplementation('org.testcontainers:k6')
testImplementation('org.testcontainers:postgresql')
testImplementation('org.testcontainers:r2dbc')
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'

runtimeOnly 'io.r2dbc:r2dbc-h2'
runtimeOnly 'org.postgresql:r2dbc-postgresql'
}

dependencyManagement {
Expand Down
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:

db:
image: postgres:15.3-alpine
ports:
- 5432:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: defaultdb
restart: always

adminer:
image: adminer
ports:
- 8180:8080
restart: always
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.weesvc.springboot.weesvc.domain.PlacesRepository;
import io.weesvc.springboot.weesvc.domain.model.Place;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -18,6 +19,7 @@

@RestController
@RequestMapping("/api/places")
@Slf4j
public class PlacesRestController {

@Autowired
Expand All @@ -30,6 +32,7 @@ public Flux<Place> getPlaces() {

@PostMapping
public Mono<Place> addPlace(@RequestBody Place place) {
log.info("Adding place named {}", place.getName());
final Instant now = Instant.now();
place.setCreatedAt(now);
place.setUpdatedAt(now);
Expand All @@ -38,13 +41,15 @@ public Mono<Place> addPlace(@RequestBody Place place) {

@GetMapping("{id}")
public Mono<Place> getPlaceById(@PathVariable Long id) {
log.info("Retrieving place with ID {}", id);
return placesRepository.findById(id)
.switchIfEmpty(Mono.error(new PlaceNotFoundException()));
}

@PatchMapping("{id}")
public Mono<Place> updatePlaceById(@PathVariable Long id,
@RequestBody Place updates) {
log.info("Updating place with ID {}", id);
return placesRepository.findById(id)
.switchIfEmpty(Mono.error(new PlaceNotFoundException()))
.flatMap(p -> {
Expand All @@ -68,6 +73,7 @@ public Mono<Place> updatePlaceById(@PathVariable Long id,

@DeleteMapping("{id}")
public Mono<Void> deletePlaceById(@PathVariable Long id) {
log.info("Deleting place with ID {}", id);
return placesRepository.findById(id)
.switchIfEmpty(Mono.error(new PlaceNotFoundException()))
.then(placesRepository.deleteById(id));
Expand Down
14 changes: 9 additions & 5 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ server:
include-stacktrace: never

spring:
datasource:
url: r2dbc:h2:mem:placesdb
driverClassName: org.h2.Driver
username: sa
password: ''
r2dbc:
url: r2dbc:postgres://localhost:5432/defaultdb
username: postgres
password: postgres

r2dbc:
migrate:
resources-paths:
- classpath:/db/migration/*.sql

logging:
level:
root: INFO
io.weesvc.springboot.weesvc.api: DEBUG
6 changes: 3 additions & 3 deletions src/main/resources/db/migration/V1__create_places.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
CREATE TABLE places
(
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR UNIQUE NOT NULL,
description VARCHAR,
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT,
latitude REAL,
longitude REAL,
created_at TIMESTAMP NOT NULL,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.weesvc.springboot.weesvc.api;

import io.weesvc.springboot.weesvc.WeesvcApplication;
import io.weesvc.springboot.weesvc.domain.PlacesRepository;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.testcontainers.Testcontainers;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.k6.K6Container;
import org.testcontainers.utility.MountableFile;

import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(
classes = { WeesvcApplication.class, PlacesRestController.class, PlacesRepository.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "spring.r2dbc.url=r2dbc:tc:postgresql:///testdb?TC_IMAGE_TAG=15.3-alpine" }
)
public class APIContractTests {

@LocalServerPort
private Integer port;

@Test
public void validateAPIContract() throws Exception {

Testcontainers.exposeHostPorts(port);
try (
K6Container container = new K6Container("grafana/k6:0.49.0")
.withTestScript(MountableFile.forClasspathResource("api-compliance.js"))
.withScriptVar("HOST", "host.testcontainers.internal")
.withScriptVar("PORT", port.toString())
.withCmdOptions("--no-usage-report")
) {
container.start();

WaitingConsumer consumer = new WaitingConsumer();
container.followOutput(consumer);

// Wait for test script results to be collected
consumer.waitUntil(
frame -> frame.getUtf8String().contains("iteration_duration"),
1,
TimeUnit.MINUTES
);

assertThat(container.getLogs()).doesNotContain("thresholds on metrics 'checks' have been crossed");
// Force a "passing" test to fail to see output results from k6 Test
// assertThat(container.getLogs()).contains("thresholds on metrics 'checks' have been crossed");

}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.weesvc.springboot.weesvc.api;

import io.weesvc.springboot.weesvc.WeesvcApplication;
import io.weesvc.springboot.weesvc.domain.PlacesRepository;
import io.weesvc.springboot.weesvc.domain.model.Place;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.Duration;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(
classes = { WeesvcApplication.class, PlacesRestController.class, PlacesRepository.class },
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "spring.r2dbc.url=r2dbc:tc:postgresql:///testdb?TC_IMAGE_TAG=15.3-alpine" }
)
public class PlacesRestControllerTests {

private static final Duration ONE_SECOND_DURATION = Duration.ofSeconds(1);

@Autowired
PlacesRestController fixture;

@Test
void getPlaces() {
final List<Place> places =
fixture.getPlaces().collectList().block(ONE_SECOND_DURATION);
assertTrue(places.size() > 0, "Expecting a non-empty list of places");
}

@Test
void getPlaceByID() {
final Place place = fixture.getPlaceById(6L).block(ONE_SECOND_DURATION);
assertNotNull(place, "Expecting seeded place with id=6");
assertEquals(6L, place.getId());
assertEquals("MIA", place.getName());
assertEquals("Miami International Airport, FL, USA", place.getDescription());
assertEquals(25.79516F, place.getLatitude());
assertEquals(-80.27959F, place.getLongitude());
assertNotNull(place.getCreatedAt(), "Created date should not be nullable");
assertNotNull(place.getUpdatedAt(), "Updated date should not be nullable");
}

@Test
void createPlace() {
final Place newPlace = Place.builder()
.name("Kerid Crater")
.description("Kerid Crater, Iceland")
.latitude(64.04126F)
.longitude(-20.88530F)
.build();

final Place created = fixture.addPlace(newPlace).block();
assertNotNull(created.getId(), "ID should be autoincremented");
assertEquals(newPlace.getName(), created.getName());
assertEquals(newPlace.getDescription(), created.getDescription());
assertEquals(newPlace.getLatitude(), created.getLatitude());
assertEquals(newPlace.getLongitude(), created.getLongitude());
assertNotNull(created.getCreatedAt(), "Created date should not be nullable");
assertNotNull(created.getUpdatedAt(), "Updated date should not be nullable");
}

@Test
void updatePlaceByID() {
final Place original = fixture.getPlaceById(7L).block(ONE_SECOND_DURATION);
assertNotNull(original, "Expecting seeded place with id=7");

final Place changes = original.toBuilder()
.name("The Alamo")
.description("The Alamo, San Antonio, TX, USA")
.latitude(29.42590F)
.longitude(-98.48625F)
.build();
final Place updated = fixture.updatePlaceById(original.getId(), changes).block(ONE_SECOND_DURATION);
assertEquals(changes.getId(), updated.getId());
assertEquals(changes.getName(), updated.getName());
assertEquals(changes.getDescription(), updated.getDescription());
assertEquals(changes.getLatitude(), updated.getLatitude());
assertEquals(changes.getLongitude(), updated.getLongitude());
assertEquals(changes.getCreatedAt(), updated.getCreatedAt());
assertNotEquals(changes.getUpdatedAt(), updated.getUpdatedAt(), "Updated date should be changed");
}

@Test
void deletePlaceByID() {
final Long deleteID = 1L;
fixture.deletePlaceById(deleteID).block(ONE_SECOND_DURATION);

Exception e = assertThrows(RuntimeException.class, () -> {
fixture.getPlaceById(deleteID).block(ONE_SECOND_DURATION);
});
assertEquals(PlaceNotFoundException.class, e.getCause().getClass());
}

}
Loading

0 comments on commit 7081cbc

Please sign in to comment.