Skip to content

Commit cedc62b

Browse files
authored
Add initial integration testing using functional scenarios. (#22)
* Add initial integration testing using functional scenarios. * Simplify the testing API a bit. * Add integration testing on the budget and expense creation and patching.
1 parent cc0be3d commit cedc62b

14 files changed

+750
-0
lines changed

fintrack-api/build.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ micronaut {
1111
testRuntime("junit5")
1212
}
1313

14+
val integration by sourceSets.creating
15+
16+
configurations[integration.implementationConfigurationName].extendsFrom(configurations.testImplementation.get())
17+
configurations[integration.runtimeOnlyConfigurationName].extendsFrom(configurations.testRuntimeOnly.get())
18+
19+
tasks.register<Test>("itTest") {
20+
description = "Runs the integration tests."
21+
group = "verification"
22+
23+
testClassesDirs = integration.output.classesDirs
24+
classpath = configurations[integration.runtimeClasspathConfigurationName] + integration.output + sourceSets.main.get().output
25+
26+
shouldRunAfter(tasks.test)
27+
}
28+
29+
tasks.check {
30+
dependsOn("itTest")
31+
}
32+
1433
dependencies {
1534
annotationProcessor(mn.micronaut.openapi.asProvider())
1635
annotationProcessor(mn.micronaut.http.validation)
@@ -61,4 +80,8 @@ dependencies {
6180
testImplementation(mn.micronaut.test.rest.assured)
6281
testImplementation(mn.micronaut.test.junit5)
6382
testImplementation(libs.bundles.junit)
83+
84+
configurations["integrationImplementation"](libs.bundles.junit)
85+
configurations["integrationImplementation"](mn.micronaut.test.junit5)
86+
configurations["integrationImplementation"](mn.micronaut.test.rest.assured)
6487
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.jongsoft.finance;
2+
3+
import com.jongsoft.finance.extension.IntegrationTest;
4+
import com.jongsoft.finance.extension.TestContext;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Order;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.time.LocalDate;
10+
11+
import static org.hamcrest.Matchers.*;
12+
13+
@IntegrationTest(phase = 1)
14+
@DisplayName("User registers and creates accounts:")
15+
public class RegisterAndAccountsTest {
16+
17+
@Test
18+
@Order(1)
19+
@DisplayName("Step 1: Create and authenticate.")
20+
void createAccount(TestContext context) {
21+
context
22+
.register("sample@e", "Zomer2020")
23+
.authenticate("sample@e", "Zomer2020");
24+
}
25+
26+
@Test
27+
@Order(2)
28+
@DisplayName("Step 2: User creates the accounts")
29+
void setupAccounts(TestContext context) {
30+
context.accounts()
31+
.create("My checking account", "This is my first account", "default")
32+
.debtor("Chicko Cointer", "The employer of the person")
33+
.creditor("Netflix", "Movie subscription service")
34+
.creditor("Guarda", "A nice little shop.")
35+
.creditor("Groceries are us", "A grocery shop.");
36+
}
37+
38+
@Test
39+
@Order(3)
40+
@DisplayName("Step 3: User adds a budget")
41+
void addBudget(TestContext context) {
42+
var now = LocalDate.now();
43+
context.budgets()
44+
.create(2021, 1, 1000.00)
45+
.createExpense("Rent", 500.00)
46+
.createExpense("Groceries", 200.00)
47+
.validateBudget(2021, 1, budget -> budget
48+
.body("income", equalTo(1000.00F))
49+
.body("expenses.size()", equalTo(2))
50+
.body("expenses.name", hasItems("Rent", "Groceries"))
51+
.body("expenses.expected", hasItems(500.00F, 200F)))
52+
.updateIncome(2200)
53+
.updateExpense("Rent", 600.00)
54+
.updateExpense("Groceries", 250.00)
55+
.createExpense("Car", 300.00)
56+
.validateBudget(now.getYear(), now.getMonthValue(), budget -> budget
57+
.body("income", equalTo(2200.00F))
58+
.body("expenses.size()", equalTo(3))
59+
.body("expenses.name", hasItems("Rent", "Groceries", "Car"))
60+
.body("expenses.expected", hasItems(600.00F, 250.00F, 300.00F)))
61+
.validateBudget(2021, 1, budget -> budget
62+
.body("income", equalTo(1000.00F))
63+
.body("period.until", equalTo(now.withDayOfMonth(1).toString()))
64+
.body("expenses.size()", equalTo(2))
65+
.body("expenses.name", hasItems("Rent", "Groceries"))
66+
.body("expenses.expected", hasItems(500.00F, 200F)));
67+
}
68+
69+
@Test
70+
@Order(4)
71+
@DisplayName("Step 4: User loads the account pages")
72+
void validateAccount(TestContext context) {
73+
context.accounts()
74+
.own(response -> response
75+
.body("size()", equalTo(1))
76+
.body("[0].name", equalTo("My checking account")))
77+
.creditors(response -> response
78+
.body("info.records", equalTo(3))
79+
.body("content.name", hasItems("Groceries are us", "Guarda")))
80+
.debtors(response -> response
81+
.body("info.records", equalTo(1))
82+
.body("content.name", hasItems("Chicko Cointer")));
83+
}
84+
85+
@Test
86+
@Order(5)
87+
@DisplayName("Step 4: Update the shopping account with image")
88+
void editShoppingAccount(TestContext context) {
89+
var uploadId = context.upload(RegisterAndAccountsTest.class.getResourceAsStream("/assets/account1.svg"));
90+
91+
context.accounts()
92+
.locate("Groceries are us", account -> account
93+
.fetch(response -> response
94+
.body("name", equalTo("Groceries are us"))
95+
.body("iconFileCode", nullValue()))
96+
.icon(uploadId)
97+
.fetch(response -> response
98+
.body("name", equalTo("Groceries are us"))
99+
.body("iconFileCode", equalTo(uploadId))));
100+
}
101+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.jongsoft.finance;
2+
3+
import com.jongsoft.finance.extension.AccountContext;
4+
import com.jongsoft.finance.extension.IntegrationTest;
5+
import com.jongsoft.finance.extension.TestContext;
6+
import org.apache.commons.lang3.mutable.MutableObject;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.time.LocalDate;
11+
12+
import static org.hamcrest.Matchers.equalTo;
13+
import static org.hamcrest.Matchers.hasItem;
14+
15+
@IntegrationTest(phase = 2)
16+
@DisplayName("User creates the initial transactions:")
17+
public class TransactionTest {
18+
19+
@Test
20+
@DisplayName("Book the first salary income")
21+
void firstIncome(TestContext context) {
22+
context.authenticate("sample@e", "Zomer2020");
23+
24+
var checkingAccount = new MutableObject<AccountContext.Account>();
25+
var employer = new MutableObject<AccountContext.Account>();
26+
27+
context.accounts()
28+
.locate("My checking account", checkingAccount::setValue)
29+
.locate("Chicko Cointer", employer::setValue);
30+
31+
context.transactions()
32+
.create(
33+
checkingAccount.getValue(),
34+
employer.getValue(),
35+
1000,
36+
"First income",
37+
LocalDate.parse("2020-01-01"))
38+
.list(LocalDate.parse("2020-01-01"), response -> response
39+
.body("info.records", equalTo(1))
40+
.body("content.amount", hasItem(1000.0f)));
41+
}
42+
43+
@Test
44+
@DisplayName("Buy groceries several times")
45+
void spendMoney(TestContext context) {
46+
context.authenticate("sample@e", "Zomer2020");
47+
48+
var checkingAccount = new MutableObject<AccountContext.Account>();
49+
var groceryStore = new MutableObject<AccountContext.Account>();
50+
51+
context.accounts()
52+
.locate("My checking account", checkingAccount::setValue)
53+
.locate("Groceries are us", groceryStore::setValue);
54+
55+
context.transactions()
56+
.create(
57+
checkingAccount.getValue(),
58+
groceryStore.getValue(),
59+
22.32,
60+
"Groceries",
61+
LocalDate.parse("2020-01-02"))
62+
.create(
63+
checkingAccount.getValue(),
64+
groceryStore.getValue(),
65+
15.00,
66+
"Groceries",
67+
LocalDate.parse("2020-01-03"))
68+
.create(
69+
checkingAccount.getValue(),
70+
groceryStore.getValue(),
71+
10.00,
72+
"Groceries",
73+
LocalDate.parse("2020-02-04"))
74+
.list(LocalDate.parse("2020-01-02"), response -> response
75+
.body("info.records", equalTo(1))
76+
.body("content.amount", hasItem(22.32f)))
77+
.list(LocalDate.parse("2020-01-03"), response -> response
78+
.body("info.records", equalTo(1))
79+
.body("content.amount", hasItem(15f)));
80+
}
81+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.jongsoft.finance.extension;
2+
3+
import io.restassured.response.ValidatableResponse;
4+
import io.restassured.specification.RequestSpecification;
5+
import io.restassured.specification.ResponseSpecification;
6+
7+
import java.util.List;
8+
import java.util.function.Consumer;
9+
10+
public class AccountContext {
11+
12+
private final RequestSpecification requestSpecification;
13+
14+
public class Account {
15+
private final int id;
16+
17+
private Account(String name) {
18+
this.id = requestSpecification
19+
.get("/accounts/all")
20+
.then()
21+
.statusCode(200)
22+
.extract()
23+
.jsonPath()
24+
.<Integer>getList("findAll { it.name == '%s' }.id".formatted(name))
25+
.getFirst();
26+
}
27+
28+
public Account fetch(Consumer<ValidatableResponse> validator) {
29+
validator.accept(requestSpecification
30+
.pathParam("id", id)
31+
.get("/accounts/{id}")
32+
.then()
33+
.statusCode(200));
34+
return this;
35+
}
36+
37+
public Account icon(String uploadId) {
38+
requestSpecification
39+
.body("""
40+
{
41+
"fileCode": "%s"
42+
}""".formatted(uploadId))
43+
.pathParam("id", id)
44+
.post("/accounts/{id}/image")
45+
.then()
46+
.statusCode(200);
47+
return this;
48+
}
49+
50+
long getId() {
51+
return id;
52+
}
53+
}
54+
55+
AccountContext(RequestSpecification restAssured) {
56+
this.requestSpecification = restAssured;
57+
}
58+
59+
public AccountContext create(String name, String description, String type) {
60+
requestSpecification
61+
.body("""
62+
{
63+
"name": "%s",
64+
"description": "%s",
65+
"currency": "EUR",
66+
"type": "%s"
67+
}
68+
""".formatted(name, description, type))
69+
.put("/accounts")
70+
.then()
71+
.statusCode(200);
72+
73+
return this;
74+
}
75+
76+
public AccountContext own(Consumer<ValidatableResponse> validator) {
77+
var response = requestSpecification.get("/accounts/my-own")
78+
.then()
79+
.statusCode(200);
80+
validator.accept(response);
81+
return this;
82+
}
83+
84+
public AccountContext debtor(String name, String description) {
85+
return create(name, description, "debtor");
86+
}
87+
88+
public AccountContext debtors(Consumer<ResponseSpecification> validator) {
89+
validator.accept(list(List.of("debtor")));
90+
return this;
91+
}
92+
93+
public AccountContext creditor(String name, String description) {
94+
return create(name, description, "creditor");
95+
}
96+
97+
public AccountContext creditors(Consumer<ResponseSpecification> validator) {
98+
validator.accept(list(List.of("creditor")));
99+
return this;
100+
}
101+
102+
public AccountContext locate(String name, Consumer<Account> account) {
103+
account.accept(new Account(name));
104+
return this;
105+
}
106+
107+
private ResponseSpecification list(List<String> types) {
108+
return requestSpecification
109+
.body("""
110+
{
111+
"accountTypes": %s
112+
}
113+
""".formatted(types))
114+
.then()
115+
.statusCode(200);
116+
}
117+
}

0 commit comments

Comments
 (0)