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.