Skip to content

Commit df1be52

Browse files
committed
Add integration tests for all formatter options
Integration tests that verify XML formatting by comparing input files against expected output files for each tool option: Test cases: - Default options (id, layout_width, layout_height first) - Custom indentation (--indention) - Custom attribute indentation (--attribute-indention) - Custom attribute order (--attribute-order) - Alphabetical attribute sort (--attribute-sort) - Custom namespace order (--namespace-order) - Alphabetical namespace sort (--namespace-sort) - Combined options (multiple flags together) - Zero indentation - Valid XML output verification - Nested element structure verification Documentation: - Updated README with test structure section Total tests: 55 (31 unit + 11 integration + 13 CLI tests)
1 parent c6dddce commit df1be52

18 files changed

Lines changed: 519 additions & 0 deletions

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,67 @@ mvn verify
2828

2929
This runs all tests and code format checks.
3030

31+
```bash
32+
mvn test
33+
```
34+
35+
To run tests with code coverage report:
36+
37+
```bash
38+
mvn clean test jacoco:report
39+
```
40+
41+
42+
The coverage report will be available at `target/site/jacoco/index.html`.
43+
44+
### Test Structure
45+
46+
The project includes 55 tests organized into three test classes:
47+
48+
| Test Class | Tests | Description |
49+
|------------|-------|-------------|
50+
| `AndroidXmlOutputterTest` | 31 | Unit tests for the core XML formatting logic |
51+
| `MainTest` | 13 | Tests for CLI argument parsing and file processing |
52+
| `IntegrationTest` | 11 | End-to-end tests comparing formatted output against expected files |
53+
54+
**Integration Tests**
55+
56+
Integration tests use input/expected file pairs located in `src/test/resources/integration/`:
57+
58+
```
59+
src/test/resources/integration/
60+
├── input/ # Input XML files
61+
│ ├── default_options.xml
62+
│ ├── custom_indention.xml
63+
│ ├── custom_attribute_indention.xml
64+
│ ├── custom_attribute_order.xml
65+
│ ├── attribute_sort.xml
66+
│ ├── custom_namespace_order.xml
67+
│ ├── namespace_sort.xml
68+
│ └── combined_options.xml
69+
└── expected/ # Expected output files
70+
└── (corresponding files)
71+
```
72+
73+
Each integration test formats an input file with specific options and compares the result against the expected output file.
74+
75+
## Code Formatting
76+
77+
This project uses [Spotless](https://github.com/diffplug/spotless) with Google Java Format for code formatting.
78+
79+
To check if code is properly formatted:
80+
81+
```bash
82+
mvn spotless:check
83+
```
84+
85+
To automatically format all Java files:
86+
87+
```bash
88+
mvn spotless:apply
89+
```
90+
91+
3192
## Usage
3293

3394
To view available command line options:
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package com.bytehamster.androidxmlformatter;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.jdom.Document;
6+
import org.jdom.input.SAXBuilder;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.io.ByteArrayOutputStream;
11+
import java.io.InputStream;
12+
import java.io.StringWriter;
13+
import java.nio.charset.StandardCharsets;
14+
15+
/**
16+
* Integration tests that verify XML formatting with various options by comparing input XML files
17+
* against expected output XML files.
18+
*/
19+
class IntegrationTest {
20+
21+
private static final String INTEGRATION_DIR = "/integration/";
22+
private static final String INPUT_SUFFIX = "_input.xml";
23+
private static final String EXPECTED_SUFFIX = "_expected.xml";
24+
25+
private String loadResource(String path) throws Exception {
26+
try (InputStream is = getClass().getResourceAsStream(path)) {
27+
if (is == null) {
28+
throw new IllegalArgumentException("Resource not found: " + path);
29+
}
30+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
31+
int bytesRead;
32+
byte[] data = new byte[1024];
33+
while ((bytesRead = is.read(data, 0, data.length)) != -1) {
34+
buffer.write(data, 0, bytesRead);
35+
}
36+
return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
37+
}
38+
}
39+
40+
private Document parseResource(String path) throws Exception {
41+
try (InputStream is = getClass().getResourceAsStream(path)) {
42+
if (is == null) {
43+
throw new IllegalArgumentException("Resource not found: " + path);
44+
}
45+
return new SAXBuilder().build(is);
46+
}
47+
}
48+
49+
private String formatDocument(AndroidXmlOutputter outputter, Document doc) throws Exception {
50+
StringWriter writer = new StringWriter();
51+
outputter.output(doc, writer);
52+
return writer.toString();
53+
}
54+
55+
private void assertFormattedOutputMatches(
56+
String testName,
57+
int indention,
58+
int attributeIndention,
59+
String[] namespaceOrder,
60+
String[] attributeOrder,
61+
boolean attributeSort,
62+
boolean namespaceSort)
63+
throws Exception {
64+
65+
Document inputDoc = parseResource(INTEGRATION_DIR + testName + INPUT_SUFFIX);
66+
String expected = loadResource(INTEGRATION_DIR + testName + EXPECTED_SUFFIX);
67+
68+
AndroidXmlOutputter outputter = new AndroidXmlOutputter(
69+
indention,
70+
attributeIndention,
71+
namespaceOrder,
72+
attributeOrder,
73+
attributeSort,
74+
namespaceSort);
75+
76+
String actual = formatDocument(outputter, inputDoc);
77+
78+
// Normalize line endings for comparison
79+
expected = expected.replace("\r\n", "\n").trim();
80+
actual = actual.replace("\r\n", "\n").trim();
81+
82+
assertEquals(expected, actual, "Formatted output should match expected for: " + testName);
83+
}
84+
85+
// === Default Options Test ===
86+
87+
@Test
88+
@DisplayName("Default options: id, layout_width, layout_height first with 4-space indentation")
89+
void testDefaultOptions() throws Exception {
90+
assertFormattedOutputMatches(
91+
"default_options",
92+
4, // indention
93+
4, // attribute indention
94+
new String[] { "android" }, // namespace order
95+
new String[] { "id", "layout_width", "layout_height" }, // attribute order
96+
false, // attribute sort
97+
false // namespace sort
98+
);
99+
}
100+
101+
// === Custom Indentation Test ===
102+
103+
@Test
104+
@DisplayName("Custom indentation: 2 spaces instead of 4")
105+
void testCustomIndention() throws Exception {
106+
assertFormattedOutputMatches(
107+
"custom_indention",
108+
2, // indention
109+
2, // attribute indention
110+
new String[] { "android" }, // namespace order
111+
new String[] { "id", "layout_width", "layout_height" }, // attribute order
112+
false, // attribute sort
113+
false // namespace sort
114+
);
115+
}
116+
117+
// === Custom Attribute Indentation Test ===
118+
119+
@Test
120+
@DisplayName("Custom attribute indentation: 8 spaces for attributes")
121+
void testCustomAttributeIndention() throws Exception {
122+
assertFormattedOutputMatches(
123+
"custom_attribute_indention",
124+
4, // indention
125+
8, // attribute indention
126+
new String[] { "android" }, // namespace order
127+
new String[] { "id", "layout_width", "layout_height" }, // attribute order
128+
false, // attribute sort
129+
false // namespace sort
130+
);
131+
}
132+
133+
// === Custom Attribute Order Test ===
134+
135+
@Test
136+
@DisplayName("Custom attribute order: text, background first")
137+
void testCustomAttributeOrder() throws Exception {
138+
assertFormattedOutputMatches(
139+
"custom_attribute_order",
140+
4, // indention
141+
4, // attribute indention
142+
new String[] { "android" }, // namespace order
143+
new String[] { "text", "background" }, // attribute order
144+
false, // attribute sort
145+
false // namespace sort
146+
);
147+
}
148+
149+
// === Alphabetical Attribute Sort Test ===
150+
151+
@Test
152+
@DisplayName("Attribute sort: alphabetical ordering of attributes")
153+
void testAttributeSort() throws Exception {
154+
assertFormattedOutputMatches(
155+
"attribute_sort",
156+
4, // indention
157+
4, // attribute indention
158+
new String[] { "android" }, // namespace order
159+
new String[] {}, // attribute order (empty for pure alphabetical)
160+
true, // attribute sort
161+
false // namespace sort
162+
);
163+
}
164+
165+
// === Custom Namespace Order Test ===
166+
167+
@Test
168+
@DisplayName("Custom namespace order: tools, app, android")
169+
void testCustomNamespaceOrder() throws Exception {
170+
assertFormattedOutputMatches(
171+
"custom_namespace_order",
172+
4, // indention
173+
4, // attribute indention
174+
new String[] { "tools", "app", "android" }, // namespace order
175+
new String[] { "id", "layout_width", "layout_height" }, // attribute order
176+
false, // attribute sort
177+
false // namespace sort
178+
);
179+
}
180+
181+
// === Alphabetical Namespace Sort Test ===
182+
183+
@Test
184+
@DisplayName("Namespace sort: alphabetical ordering of namespaces")
185+
void testNamespaceSort() throws Exception {
186+
assertFormattedOutputMatches(
187+
"namespace_sort",
188+
4, // indention
189+
4, // attribute indention
190+
new String[] {}, // namespace order (empty for pure alphabetical)
191+
new String[] { "id", "layout_width", "layout_height" }, // attribute order
192+
false, // attribute sort
193+
true // namespace sort
194+
);
195+
}
196+
197+
// === Combined Options Test ===
198+
199+
@Test
200+
@DisplayName("Combined options: 2-space indent, 6-space attr indent, both sorts enabled")
201+
void testCombinedOptions() throws Exception {
202+
assertFormattedOutputMatches(
203+
"combined_options",
204+
2, // indention
205+
6, // attribute indention
206+
new String[] {}, // namespace order (empty for alphabetical)
207+
new String[] {}, // attribute order (empty for alphabetical)
208+
true, // attribute sort
209+
true // namespace sort
210+
);
211+
}
212+
213+
// === Additional Integration Tests ===
214+
215+
@Test
216+
@DisplayName("Zero indentation: no indentation for elements or attributes")
217+
void testZeroIndentation() throws Exception {
218+
Document inputDoc = parseResource(INTEGRATION_DIR + "default_options" + INPUT_SUFFIX);
219+
220+
AndroidXmlOutputter outputter = new AndroidXmlOutputter(
221+
0,
222+
0,
223+
new String[] { "android" },
224+
new String[] { "id", "layout_width", "layout_height" },
225+
false,
226+
false);
227+
228+
String result = formatDocument(outputter, inputDoc);
229+
230+
// With zero indentation, child elements should not be indented
231+
assertTrue(result.contains("<Button"), "Should contain Button element");
232+
assertTrue(result.contains("<TextView"), "Should contain TextView element");
233+
assertFalse(
234+
result.contains("\n <Button"),
235+
"Button should not be indented with 4 spaces when indentation is 0");
236+
}
237+
238+
@Test
239+
@DisplayName("Verify output is valid XML that can be parsed")
240+
void testOutputIsValidXml() throws Exception {
241+
Document inputDoc = parseResource(INTEGRATION_DIR + "default_options" + INPUT_SUFFIX);
242+
243+
AndroidXmlOutputter outputter = new AndroidXmlOutputter(
244+
4,
245+
4,
246+
new String[] { "android" },
247+
new String[] { "id", "layout_width", "layout_height" },
248+
false,
249+
false);
250+
251+
String result = formatDocument(outputter, inputDoc);
252+
253+
// Parse the output to verify it's valid XML
254+
SAXBuilder builder = new SAXBuilder();
255+
Document parsedDoc = builder.build(new java.io.StringReader(result));
256+
257+
assertNotNull(parsedDoc, "Output should be parseable as XML");
258+
assertEquals(
259+
"LinearLayout",
260+
parsedDoc.getRootElement().getName(),
261+
"Root element should be LinearLayout");
262+
}
263+
264+
@Test
265+
@DisplayName("Nested elements maintain correct structure")
266+
void testNestedElementsStructure() throws Exception {
267+
Document inputDoc = parseResource(INTEGRATION_DIR + "default_options" + INPUT_SUFFIX);
268+
269+
AndroidXmlOutputter outputter = new AndroidXmlOutputter(
270+
4,
271+
4,
272+
new String[] { "android" },
273+
new String[] { "id", "layout_width", "layout_height" },
274+
false,
275+
false);
276+
277+
String result = formatDocument(outputter, inputDoc);
278+
279+
// Verify nested structure
280+
assertTrue(result.contains("<LinearLayout"), "Should contain LinearLayout");
281+
assertTrue(result.contains("</LinearLayout>"), "Should have closing LinearLayout tag");
282+
assertTrue(result.contains("<Button"), "Should contain Button");
283+
assertTrue(result.contains("<TextView"), "Should contain TextView");
284+
285+
// Verify proper nesting (Button and TextView are inside LinearLayout)
286+
int linearLayoutStart = result.indexOf("<LinearLayout");
287+
int linearLayoutEnd = result.indexOf("</LinearLayout>");
288+
int buttonPos = result.indexOf("<Button");
289+
int textViewPos = result.indexOf("<TextView");
290+
291+
assertTrue(
292+
buttonPos > linearLayoutStart && buttonPos < linearLayoutEnd,
293+
"Button should be inside LinearLayout");
294+
assertTrue(
295+
textViewPos > linearLayoutStart && textViewPos < linearLayoutEnd,
296+
"TextView should be inside LinearLayout");
297+
}
298+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<View
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
android:alpha="0.5"
5+
android:background="#FFF"
6+
android:elevation="4dp"
7+
android:id="@+id/view"
8+
android:zIndex="1" />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<View xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:zIndex="1"
4+
android:alpha="0.5"
5+
android:background="#FFF"
6+
android:elevation="4dp"
7+
android:id="@+id/view" />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<View
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
xmlns:app="http://schemas.android.com/apk/res-auto"
5+
android:alpha="0.5"
6+
android:id="@+id/view"
7+
android:zIndex="1"
8+
app:customA="a"
9+
app:customZ="z" />

0 commit comments

Comments
 (0)