Skip to content

Commit c9845f7

Browse files
authored
refactor: consolidate schema readers and add comprehensive documentation (#48)
* refactor: consolidate schema readers and add comprehensive documentation - Extract common functionality into SchemaReaderBase class - Consolidate duplicate code across MySql, PostgreSql, and SqlServer schema readers - Add architecture documentation (pipeline, fingerprinting, overview) - Add use-case documentation (CI/CD patterns, enterprise usage) - Add large-schema case study - Update CONTRIBUTING.md with development guidelines
1 parent 22fcade commit c9845f7

15 files changed

Lines changed: 2624 additions & 2271 deletions

File tree

CONTRIBUTING.md

Lines changed: 225 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ When adding or modifying tasks:
120120

121121
### Testing
122122

123+
JD.Efcpt.Build uses **TinyBDD** for behavior-driven testing. All tests follow a consistent Given-When-Then pattern.
124+
125+
#### Testing Framework
126+
127+
We use **TinyBDD** for all tests (not traditional xUnit Arrange-Act-Assert). This provides:
128+
- ✅ Clear behavior specifications
129+
- ✅ Readable test scenarios
130+
- ✅ Consistent patterns across the codebase
131+
- ✅ Self-documenting tests
132+
123133
#### Running Tests
124134

125135
```bash
@@ -129,47 +139,238 @@ dotnet test
129139
# Run with detailed output
130140
dotnet test -v detailed
131141

132-
# Run specific test
133-
dotnet test --filter "FullyQualifiedName~TestName"
142+
# Run specific test category
143+
dotnet test --filter "FullyQualifiedName~SchemaReader"
144+
145+
# Run with code coverage
146+
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
134147
```
135148

136-
#### Writing Tests
149+
#### Writing Tests with TinyBDD
150+
151+
**Test Structure:**
152+
153+
```csharp
154+
using TinyBDD.Xunit;
155+
using Xunit;
156+
157+
[Feature("Component: brief description of functionality")]
158+
[Collection(nameof(AssemblySetup))]
159+
public sealed class ComponentTests(ITestOutputHelper output) : TinyBddXunitBase(output)
160+
{
161+
// Define state records
162+
private sealed record SetupState(
163+
string InputValue,
164+
ITestOutputHelper Output);
165+
166+
private sealed record ExecutionResult(
167+
bool Success,
168+
string Output,
169+
Exception? Error = null);
170+
171+
[Scenario("Description of specific behavior")]
172+
[Fact]
173+
public async Task Scenario_Name()
174+
{
175+
await Given("context setup", () => new SetupState("test-value", Output))
176+
.When("action is performed", state => PerformAction(state))
177+
.Then("expected outcome occurs", result => result.Success)
178+
.And("additional assertion", result => result.Output == "expected")
179+
.Finally(result => CleanupResources(result))
180+
.AssertPassed();
181+
}
182+
183+
private static ExecutionResult PerformAction(SetupState state)
184+
{
185+
try
186+
{
187+
// Execute the action being tested
188+
var output = DoSomething(state.InputValue);
189+
return new ExecutionResult(true, output);
190+
}
191+
catch (Exception ex)
192+
{
193+
return new ExecutionResult(false, "", ex);
194+
}
195+
}
196+
197+
private static void CleanupResources(ExecutionResult result)
198+
{
199+
// Clean up any resources
200+
}
201+
}
202+
```
137203

138-
- Add tests for new features
139-
- Test both success and error scenarios
140-
- Use descriptive test names: `Should_ExpectedBehavior_When_Condition`
141-
- Keep tests isolated and independent
142-
- Mock external dependencies
204+
#### Testing Best Practices
143205

144-
Example test structure:
206+
**DO:**
207+
- ✅ Use TinyBDD for all new tests
208+
- ✅ Write descriptive scenario names (e.g., "Should detect changed fingerprint when DACPAC modified")
209+
- ✅ Use state records for Given context
210+
- ✅ Use result records for When outcomes
211+
- ✅ Test both success and failure paths
212+
- ✅ Clean up resources in `Finally` blocks
213+
- ✅ Use meaningful assertion messages
214+
215+
**DON'T:**
216+
- ❌ Use traditional Arrange-Act-Assert (use Given-When-Then)
217+
- ❌ Skip the `Finally` block if cleanup is needed
218+
- ❌ Write tests without clear scenarios
219+
- ❌ Test implementation details (test behavior)
220+
- ❌ Create inter-dependent tests
221+
222+
#### Testing Patterns
223+
224+
**Pattern 1: Simple Value Transformation**
145225

146226
```csharp
227+
[Scenario("Should compute fingerprint from byte array")]
147228
[Fact]
148-
public void Should_StageTemplates_When_TemplateDirectoryExists()
229+
public async Task Computes_fingerprint_from_bytes()
149230
{
150-
// Arrange
151-
var task = new StageEfcptInputs
152-
{
153-
OutputDir = testDir,
154-
TemplateDir = sourceTemplateDir,
155-
// ... other properties
156-
};
231+
await Given("byte array with known content", () => new byte[] { 1, 2, 3, 4 })
232+
.When("computing fingerprint", bytes => ComputeFingerprint(bytes))
233+
.Then("fingerprint is deterministic", fp => !string.IsNullOrEmpty(fp))
234+
.And("fingerprint has expected format", fp => fp.Length == 16)
235+
.AssertPassed();
236+
}
237+
```
157238

158-
// Act
159-
var result = task.Execute();
239+
**Pattern 2: File System Operations**
160240

161-
// Assert
162-
Assert.True(result);
163-
Assert.True(Directory.Exists(expectedStagedPath));
241+
```csharp
242+
[Scenario("Should create output directory when it doesn't exist")]
243+
[Fact]
244+
public async Task Creates_missing_output_directory()
245+
{
246+
await Given("non-existent directory path", () =>
247+
{
248+
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
249+
return new SetupState(path, Output);
250+
})
251+
.When("ensuring directory exists", state =>
252+
{
253+
Directory.CreateDirectory(state.Path);
254+
return new Result(Directory.Exists(state.Path), state.Path);
255+
})
256+
.Then("directory is created", result => result.Exists)
257+
.Finally(result =>
258+
{
259+
if (Directory.Exists(result.Path))
260+
Directory.Delete(result.Path, true);
261+
})
262+
.AssertPassed();
164263
}
165264
```
166265

266+
**Pattern 3: Exception Testing**
267+
268+
```csharp
269+
[Scenario("Should throw when connection string is invalid")]
270+
[Fact]
271+
public async Task Throws_on_invalid_connection_string()
272+
{
273+
await Given("invalid connection string", () => "not-a-valid-connection-string")
274+
.When("reading schema", connectionString =>
275+
{
276+
try
277+
{
278+
reader.ReadSchema(connectionString);
279+
return (false, null as Exception);
280+
}
281+
catch (Exception ex)
282+
{
283+
return (true, ex);
284+
}
285+
})
286+
.Then("exception is thrown", result => result.Item1)
287+
.And("exception message is descriptive", result =>
288+
result.Item2!.Message.Contains("connection") ||
289+
result.Item2!.Message.Contains("invalid"))
290+
.AssertPassed();
291+
}
292+
```
293+
294+
**Pattern 4: Integration Tests with Testcontainers**
295+
296+
```csharp
297+
[Feature("PostgreSqlSchemaReader: integration with real database")]
298+
[Collection(nameof(PostgreSqlContainer))]
299+
public sealed class PostgreSqlSchemaIntegrationTests(
300+
PostgreSqlFixture fixture,
301+
ITestOutputHelper output) : TinyBddXunitBase(output)
302+
{
303+
[Scenario("Should read schema from PostgreSQL database")]
304+
[Fact]
305+
public async Task Reads_schema_from_postgres()
306+
{
307+
await Given("PostgreSQL database with test schema", () => fixture.ConnectionString)
308+
.When("reading schema", cs => new PostgreSqlSchemaReader().ReadSchema(cs))
309+
.Then("schema contains expected tables", schema => schema.Tables.Count > 0)
310+
.And("tables have columns", schema => schema.Tables.All(t => t.Columns.Any()))
311+
.AssertPassed();
312+
}
313+
}
314+
```
315+
316+
#### Test Coverage Goals
317+
318+
| Component | Target | Current |
319+
|-----------|--------|---------|
320+
| **MSBuild Tasks** | 95%+ | ~90% |
321+
| **Schema Readers** | 90%+ | ~85% |
322+
| **Resolution Chains** | 90%+ | ~88% |
323+
| **Utilities** | 85%+ | ~82% |
324+
325+
#### Integration Testing
326+
327+
**Database Provider Tests:**
328+
- Use Testcontainers for SQL Server, PostgreSQL, MySQL
329+
- Use in-memory SQLite for fast tests
330+
- Mock unavailable providers (Snowflake requires LocalStack Pro)
331+
332+
**Sample Projects:**
333+
- Create minimal test projects in `tests/TestAssets/`
334+
- Test actual MSBuild integration
335+
- Verify generated code compiles
336+
337+
#### Running Integration Tests
338+
339+
```bash
340+
# Requires Docker for Testcontainers
341+
docker info
342+
343+
# Run integration tests
344+
dotnet test --filter "Category=Integration"
345+
346+
# Run specific provider tests
347+
dotnet test --filter "FullyQualifiedName~PostgreSql"
348+
```
349+
350+
#### Debugging Tests
351+
352+
```csharp
353+
// TinyBDD provides detailed output on failure
354+
await Given("setup", CreateSetup)
355+
.When("action", Execute)
356+
.Then("assertion", result => result.IsValid)
357+
.AssertPassed();
358+
359+
// On failure, you'll see:
360+
// ❌ Scenario failed at step: Then "assertion"
361+
// Expected: True
362+
// Actual: False
363+
// State: { ... }
364+
```
365+
366+
For more details, see [TinyBDD documentation](https://github.com/ledjon-behluli/TinyBDD).
367+
167368
### Documentation
168369

169370
When contributing, please update:
170371

171372
- **README.md** - For user-facing features
172-
- **QUICKSTART.md** - For common usage scenarios
373+
- **docs/** - For detailed documentation in docs/user-guide/
173374
- **XML comments** - For all public APIs
174375
- **Code comments** - For complex logic
175376

@@ -238,7 +439,7 @@ Maintainers handle releases using this process:
238439

239440
- **GitHub Issues** - For bugs and feature requests
240441
- **GitHub Discussions** - For questions and community support
241-
- **Documentation** - Check README.md and QUICKSTART.md first
442+
- **Documentation** - Check README.md and docs/user-guide/ first
242443

243444
## Recognition
244445

JD.Efcpt.Build.sln

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1717
CONTRIBUTING.md = CONTRIBUTING.md
1818
Directory.Build.props = Directory.Build.props
1919
LICENSE = LICENSE
20-
QUICKSTART.md = QUICKSTART.md
2120
README.md = README.md
2221
EndProjectSection
2322
EndProject

0 commit comments

Comments
 (0)