Housekeeping: starting a new project
Old-timers might reminisce mvn archetype:generate. It’s too laborious in these times (updating pom compiler target by hand?) and we usually prefer our favorite IDE or Spring Initializr.
1. With intelliJ IDEA Community Edn, that’s: File > Create New Project > Gradle > Java
2. create a README.MD (Markdown wiki format):
$ cat <<EOT >> README.MD //project antecedents and platform decisions
3. One could follow Spring Boot Gradle Plugin Reference to add Spring Boot to the project.
plugins { id 'org.springframework.boot' version '2.2.6.RELEASE' }
apply plugin: 'java'
apply plugin: 'io.spring.dependency-management'
Without Spring Boot, one would add these test dependencies in build.gradle. Applying the ‘io.spring.dependency-management’ plugin resolves the dependency versions from the Spring Boot dependencies BOM (using the properties defined there), so we omit versions:
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter-api') //5.4.2
testImplementation('org.junit.jupiter:junit-jupiter-engine')
testImplementation('org.assertj:assertj-core') //:3.15.0
testImplementation('org.mockito:mockito-core') //:3.3.3
}
But with Spring Boot, you just need:
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
Also add the lombok plugin:
id 'io.freefair.lombok' version '5.0.0'
4. Setup version control with Git
a) create a .gitignore to avoid checking in .gradle etc, one directory or file per line:
$ cat <<EOT >> .gitignore .gradle .idea gradle build lombok.config EOT
b) initialize a git repo for the project, add all changes to staging and make initial commit:
$ git init $ git add -A; git status $ git commit -m “Initial commit.”
c) Make it safe to experiment and commit: create a new branch (checkout a tag) to get in ‘detached HEAD’ state:
$ git checkout -b TDD_Kindle_Clip_Parser #creates new branch $ git status
Outside-In Approach
Let’s start by identifying high-level system interfaces, modules and boundaries. The domain suggests a Kindle device interface, a ClipFile reader/parsing module, a persistence module for storing clips across cycles, a driver module for selecting books and outputting to different formats, a module for each format to be exported, a Calibre interface module.
First Feature: As a user, I want to parse all the clips in my clip file so that I can import and process them later.
I start by parsing a single clip, add the Test for that:
@DisplayName("A ClipFile Parser")
public class ClipFileParserSpec {
@Nested
@DisplayName("parses")
class ParseFileContent {
@Test
@DisplayName("a block of five lines into a valid Clipping")
void parsesBlockOfFiveLinesIntoValidClippingObject() {
ClipFileParser clipfileParser = new ClipFileParser();
Clipping clip = clipfileParser.parseClip(scanner);
assertAll("Should return a valid clipping object",
() -> assertNotNull(clip, "Clipping object is null"),
() -> assertNotNull(clip.documentTitle(), "Title missing"),
() -> assertNotNull(clip.documentAuthor(), "Author missing"),
() -> assertNotNull(clip.type(), "Clip type not set"),
() -> assertTrue(clip.location()[0].isPresent(), "Start location missing")
);
}
}
}
What I like most about this inner class mechanism for nesting, despite it’s clumsiness, is it makes the test result more readable and accessible by way of hierarchical presentation:
Test Results:
* A ClipFile Parser
* parses
* a block of five lines into a valid Clipping
When a test fails, it’s much more intuitive to decipher why.