Skip to content

Commit 87fe15a

Browse files
sj-robertrobert-sjoblom
authored andcommitted
test(bridge): add JUnit unit tests and golden-file snapshot tests
16 tests: 9 unit tests covering processChangelog behavior (skip-on-error, metadata, raw SQL, include directives, run_in_transaction, empty result, missing file error) and 7 parameterized golden-file tests comparing JSON output against committed snapshots. Also: add JUnit 5 + JSONAssert to pom.xml, enable tests in Dockerfile, make processChangelog package-private for testability.
1 parent fae4263 commit 87fe15a

23 files changed

Lines changed: 598 additions & 4 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ jobs:
9999
-v "$PWD/bridge:/build" \
100100
-w /build \
101101
maven:3.9-eclipse-temurin-21 \
102-
sh -c 'mvn package -q -DskipTests && cp target/liquibase-bridge-1.0.0.jar target/liquibase-bridge.jar'
102+
sh -c 'mvn package -q && cp target/liquibase-bridge-1.0.0.jar target/liquibase-bridge.jar'
103103
104104
- name: Upload bridge JAR
105105
uses: actions/upload-artifact@v6

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
-v "$PWD/bridge:/build" \
6767
-w /build \
6868
maven:3.9-eclipse-temurin-21 \
69-
sh -c 'mvn package -q -DskipTests && cp target/liquibase-bridge-1.0.0.jar target/liquibase-bridge.jar'
69+
sh -c 'mvn package -q && cp target/liquibase-bridge-1.0.0.jar target/liquibase-bridge.jar'
7070
7171
- name: Upload bridge JAR
7272
uses: actions/upload-artifact@v6

bridge/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN mvn dependency:go-offline -q
99

1010
# Copy source and build the shaded JAR
1111
COPY src/ src/
12-
RUN mvn package -q -DskipTests
12+
RUN mvn package -q
1313

1414
# Stage 2: Slim runtime image with just JRE and the JAR
1515
FROM eclipse-temurin:21-jre-alpine

bridge/pom.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@
3535
<artifactId>gson</artifactId>
3636
<version>${gson.version}</version>
3737
</dependency>
38+
39+
<!-- Test dependencies -->
40+
<dependency>
41+
<groupId>org.junit.jupiter</groupId>
42+
<artifactId>junit-jupiter</artifactId>
43+
<version>5.11.4</version>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.skyscreamer</groupId>
48+
<artifactId>jsonassert</artifactId>
49+
<version>1.5.3</version>
50+
<scope>test</scope>
51+
</dependency>
3852
</dependencies>
3953

4054
<build>
@@ -53,6 +67,12 @@
5367
</configuration>
5468
</plugin>
5569

70+
<plugin>
71+
<groupId>org.apache.maven.plugins</groupId>
72+
<artifactId>maven-surefire-plugin</artifactId>
73+
<version>3.5.2</version>
74+
</plugin>
75+
5676
<!-- Shade plugin to produce a fat/uber JAR with all dependencies -->
5777
<plugin>
5878
<groupId>org.apache.maven.plugins</groupId>

bridge/src/main/java/com/pgmigrationlint/LiquibaseBridge.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public static void main(String[] args) {
5858
}
5959
}
6060

61-
private static List<ChangesetEntry> processChangelog(String changelogPath) throws Exception {
61+
static List<ChangesetEntry> processChangelog(String changelogPath) throws Exception {
6262
File changelogFile = new File(changelogPath).getAbsoluteFile();
6363
if (!changelogFile.exists()) {
6464
throw new IllegalArgumentException("Changelog file not found: " + changelogFile);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.pgmigrationlint;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.GsonBuilder;
5+
import org.json.JSONException;
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.ValueSource;
8+
import org.skyscreamer.jsonassert.JSONAssert;
9+
import org.skyscreamer.jsonassert.JSONCompareMode;
10+
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.net.URISyntaxException;
14+
import java.nio.charset.StandardCharsets;
15+
import java.nio.file.Paths;
16+
import java.util.List;
17+
18+
import static org.junit.jupiter.api.Assertions.*;
19+
20+
/**
21+
* Golden-file tests that run the bridge against fixture changelogs
22+
* and compare the JSON output to committed snapshot files.
23+
*
24+
* To regenerate golden files after intentional changes, run the bridge
25+
* against each fixture XML and capture stdout as the .expected.json file.
26+
*/
27+
class GoldenFileTest {
28+
29+
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
30+
31+
@ParameterizedTest(name = "golden file: {0}")
32+
@ValueSource(strings = {
33+
"basic-create-table",
34+
"multi-change-changeset",
35+
"raw-sql",
36+
"skip-unsupported",
37+
"run-in-transaction",
38+
"mixed-ddl",
39+
"include-directive"
40+
})
41+
void goldenFileMatchesExpectedOutput(String fixtureName) throws Exception {
42+
String xmlPath = fixtureFilePath(fixtureName + ".xml");
43+
String expectedJson = loadResource("fixtures/" + fixtureName + ".expected.json");
44+
45+
List<LiquibaseBridge.ChangesetEntry> entries =
46+
LiquibaseBridge.processChangelog(xmlPath);
47+
String actualJson = GSON.toJson(entries);
48+
49+
try {
50+
JSONAssert.assertEquals(expectedJson, actualJson, JSONCompareMode.STRICT);
51+
} catch (JSONException e) {
52+
fail("JSON comparison failed for " + fixtureName + ": " + e.getMessage()
53+
+ "\n\nActual output:\n" + actualJson);
54+
}
55+
}
56+
57+
private String fixtureFilePath(String name) throws URISyntaxException {
58+
var url = getClass().getClassLoader().getResource("fixtures/" + name);
59+
assertNotNull(url, "Missing fixture: " + name);
60+
return Paths.get(url.toURI()).toString();
61+
}
62+
63+
private String loadResource(String path) throws IOException {
64+
try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) {
65+
assertNotNull(is, "Missing resource: " + path);
66+
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
67+
}
68+
}
69+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.pgmigrationlint;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.PrintStream;
7+
import java.net.URISyntaxException;
8+
import java.nio.file.Paths;
9+
import java.util.List;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
class LiquibaseBridgeTest {
14+
15+
private String fixturePath(String name) throws URISyntaxException {
16+
var url = getClass().getClassLoader().getResource("fixtures/" + name);
17+
assertNotNull(url, "Fixture not found: " + name);
18+
return Paths.get(url.toURI()).toString();
19+
}
20+
21+
@Test
22+
void basicCreateTableProducesCorrectEntries() throws Exception {
23+
List<LiquibaseBridge.ChangesetEntry> entries =
24+
LiquibaseBridge.processChangelog(fixturePath("basic-create-table.xml"));
25+
26+
assertEquals(1, entries.size());
27+
28+
LiquibaseBridge.ChangesetEntry entry = entries.get(0);
29+
assertEquals("1", entry.changeset_id);
30+
assertEquals("testauthor", entry.author);
31+
assertTrue(entry.run_in_transaction);
32+
assertTrue(entry.sql.toUpperCase().contains("CREATE TABLE"),
33+
"Expected CREATE TABLE in SQL: " + entry.sql);
34+
assertTrue(entry.sql.toLowerCase().contains("widgets"),
35+
"Expected 'widgets' in SQL: " + entry.sql);
36+
}
37+
38+
@Test
39+
void unsupportedChangesetIsSkippedOthersSucceed() throws Exception {
40+
PrintStream originalErr = System.err;
41+
ByteArrayOutputStream errCapture = new ByteArrayOutputStream();
42+
System.setErr(new PrintStream(errCapture));
43+
44+
List<LiquibaseBridge.ChangesetEntry> entries;
45+
try {
46+
entries = LiquibaseBridge.processChangelog(fixturePath("skip-unsupported.xml"));
47+
} finally {
48+
System.setErr(originalErr);
49+
}
50+
51+
List<String> ids = entries.stream().map(e -> e.changeset_id).toList();
52+
assertTrue(ids.contains("skip-1"), "skip-1 should be present");
53+
assertTrue(ids.contains("skip-3"), "skip-3 should be present");
54+
assertFalse(ids.contains("skip-2"), "loadData changeset should be skipped");
55+
56+
String stderr = errCapture.toString();
57+
assertTrue(stderr.contains("skip-2"), "Warning should mention skipped changeset id");
58+
}
59+
60+
@Test
61+
void changesetWithNoSqlProducesEmptyResult() throws Exception {
62+
List<LiquibaseBridge.ChangesetEntry> entries =
63+
LiquibaseBridge.processChangelog(fixturePath("empty-no-sql.xml"));
64+
65+
assertTrue(entries.isEmpty(),
66+
"Expected empty result for no-SQL changesets, got " + entries.size());
67+
}
68+
69+
@Test
70+
void metadataFieldsAreCorrectlyPopulated() throws Exception {
71+
List<LiquibaseBridge.ChangesetEntry> entries =
72+
LiquibaseBridge.processChangelog(fixturePath("basic-create-table.xml"));
73+
74+
assertEquals(1, entries.size());
75+
LiquibaseBridge.ChangesetEntry entry = entries.get(0);
76+
77+
assertEquals("1", entry.changeset_id);
78+
assertEquals("testauthor", entry.author);
79+
assertTrue(entry.xml_file.contains("basic-create-table.xml"),
80+
"xml_file should reference the changelog file, got: " + entry.xml_file);
81+
// Liquibase doesn't expose line numbers; bridge always defaults to 1
82+
assertEquals(1, entry.xml_line);
83+
assertTrue(entry.run_in_transaction);
84+
}
85+
86+
@Test
87+
void multiChangeChangesetProducesCombinedSql() throws Exception {
88+
List<LiquibaseBridge.ChangesetEntry> entries =
89+
LiquibaseBridge.processChangelog(fixturePath("multi-change-changeset.xml"));
90+
91+
assertEquals(1, entries.size());
92+
93+
String sql = entries.get(0).sql.toUpperCase();
94+
assertTrue(sql.contains("CREATE TABLE"), "Should contain CREATE TABLE");
95+
assertTrue(sql.contains("CREATE INDEX"), "Should contain CREATE INDEX");
96+
assertTrue(sql.contains("IDX_ORDERS_CUSTOMER"), "Should reference the index name");
97+
}
98+
99+
@Test
100+
void rawSqlTagsProduceCorrectOutput() throws Exception {
101+
List<LiquibaseBridge.ChangesetEntry> entries =
102+
LiquibaseBridge.processChangelog(fixturePath("raw-sql.xml"));
103+
104+
assertEquals(2, entries.size());
105+
106+
assertEquals("raw-1", entries.get(0).changeset_id);
107+
assertTrue(entries.get(0).sql.contains("CONCURRENTLY"),
108+
"First raw SQL should contain CONCURRENTLY");
109+
110+
assertEquals("raw-2", entries.get(1).changeset_id);
111+
assertTrue(entries.get(1).sql.contains("CHECK"),
112+
"Second raw SQL should contain CHECK");
113+
}
114+
115+
@Test
116+
void missingChangelogThrowsException() {
117+
assertThrows(IllegalArgumentException.class, () ->
118+
LiquibaseBridge.processChangelog("/nonexistent/path/changelog.xml"));
119+
}
120+
121+
@Test
122+
void runInTransactionCapturedCorrectly() throws Exception {
123+
List<LiquibaseBridge.ChangesetEntry> entries =
124+
LiquibaseBridge.processChangelog(fixturePath("run-in-transaction.xml"));
125+
126+
assertEquals(2, entries.size());
127+
128+
LiquibaseBridge.ChangesetEntry defaultTxn = entries.stream()
129+
.filter(e -> "txn-default".equals(e.changeset_id))
130+
.findFirst().orElseThrow();
131+
assertTrue(defaultTxn.run_in_transaction, "Default should be true");
132+
133+
LiquibaseBridge.ChangesetEntry falseTxn = entries.stream()
134+
.filter(e -> "txn-false".equals(e.changeset_id))
135+
.findFirst().orElseThrow();
136+
assertFalse(falseTxn.run_in_transaction, "Explicit false should be false");
137+
}
138+
139+
@Test
140+
void includeDirectiveResolvesChildFile() throws Exception {
141+
List<LiquibaseBridge.ChangesetEntry> entries =
142+
LiquibaseBridge.processChangelog(fixturePath("include-directive.xml"));
143+
144+
assertEquals(2, entries.size());
145+
146+
LiquibaseBridge.ChangesetEntry master = entries.stream()
147+
.filter(e -> "master-1".equals(e.changeset_id))
148+
.findFirst().orElseThrow();
149+
assertTrue(master.xml_file.contains("include-directive.xml"),
150+
"Master changeset should reference master file, got: " + master.xml_file);
151+
152+
LiquibaseBridge.ChangesetEntry child = entries.stream()
153+
.filter(e -> "child-1".equals(e.changeset_id))
154+
.findFirst().orElseThrow();
155+
assertTrue(child.xml_file.contains("include-child.xml"),
156+
"Child changeset should reference child file, got: " + child.xml_file);
157+
}
158+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{
3+
"changeset_id": "1",
4+
"author": "testauthor",
5+
"sql": "CREATE TABLE widgets (id BIGINT NOT NULL, name VARCHAR(200) NOT NULL, description TEXT, CONSTRAINT widgets_pkey PRIMARY KEY (id));",
6+
"xml_file": "basic-create-table.xml",
7+
"xml_line": 1,
8+
"run_in_transaction": true
9+
}
10+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
5+
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
6+
7+
<changeSet id="1" author="testauthor">
8+
<createTable tableName="widgets">
9+
<column name="id" type="BIGINT">
10+
<constraints primaryKey="true" nullable="false"/>
11+
</column>
12+
<column name="name" type="VARCHAR(200)">
13+
<constraints nullable="false"/>
14+
</column>
15+
<column name="description" type="TEXT"/>
16+
</createTable>
17+
</changeSet>
18+
19+
</databaseChangeLog>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
5+
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
6+
7+
<changeSet id="tag-1" author="alice">
8+
<tagDatabase tag="v1.0"/>
9+
</changeSet>
10+
11+
</databaseChangeLog>

0 commit comments

Comments
 (0)