├── settings.gradle ├── .gitattributes ├── 9781484230145.jpg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── bookstoread │ │ ├── BookShelfCapacityReached.java │ │ ├── Progress.java │ │ ├── BookFilter.java │ │ ├── Book.java │ │ └── BookShelf.java └── test │ └── java │ └── bookstoread │ ├── ParameterResolverSpec.java │ ├── LoggingTestExecutionExceptionHandler.java │ ├── BookShelfSpecWithRules.java │ ├── DynamicSpec.java │ ├── BookFilterTemplateSpec.java │ ├── ParameterizedBookShelfSpec.java │ ├── BookShelfProgressSpec.java │ ├── BookFilterSpec.java │ └── BookShelfSpec.java ├── errata.md ├── README.md ├── Contributing.md ├── ivy.xml ├── LICENSE.txt ├── .gitignore ├── gradlew.bat ├── gradlew └── pom.xml /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'bookstoread' 2 | 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /9781484230145.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/java-unit-testing-with-junit-5/HEAD/9781484230145.jpg -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/java-unit-testing-with-junit-5/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/bookstoread/BookShelfCapacityReached.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | public class BookShelfCapacityReached extends RuntimeException { 4 | public BookShelfCapacityReached(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /errata.md: -------------------------------------------------------------------------------- 1 | # Errata for *Book Title* 2 | 3 | On **page xx** [Summary of error]: 4 | 5 | Details of error here. Highlight key pieces in **bold**. 6 | 7 | *** 8 | 9 | On **page xx** [Summary of error]: 10 | 11 | Details of error here. Highlight key pieces in **bold**. 12 | 13 | *** -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Sep 10 13:45:54 IST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Java Unit Testing with JUnit 5*](https://www.apress.com/9781484230145) by Shekhar Gulati and Rahul Sharma (Apress, 2017). 4 | 5 | [comment]: #cover 6 | ![Cover image](9781484230145.jpg) 7 | 8 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 9 | 10 | ## Releases 11 | 12 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 13 | 14 | ## Contributions 15 | 16 | See the file Contributing.md for more information on how you can contribute to this repository. -------------------------------------------------------------------------------- /src/test/java/bookstoread/ParameterResolverSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.junit.jupiter.api.*; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | class ParameterResolverSpec { 8 | 9 | @BeforeEach 10 | void initialize(TestInfo info,TestReporter reporter) { 11 | reporter.publishEntry("Associated tags :", info.getTags().toString()); 12 | } 13 | 14 | @RepeatedTest(value = 10) 15 | @Tag("Numbers") 16 | void numberTest(RepetitionInfo info) { 17 | assertTrue(true); 18 | } 19 | 20 | @Test 21 | void nonRepeated(RepetitionInfo info) { 22 | assertTrue(true); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /src/test/java/bookstoread/LoggingTestExecutionExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.junit.jupiter.api.extension.ExtensionContext; 4 | import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; 5 | 6 | import java.util.logging.Level; 7 | import java.util.logging.Logger; 8 | 9 | public class LoggingTestExecutionExceptionHandler implements TestExecutionExceptionHandler { 10 | 11 | private Logger logger = Logger.getLogger(LoggingTestExecutionExceptionHandler.class.getName()); 12 | 13 | @Override 14 | public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable { 15 | logger.log(Level.INFO, "Exception thrown ", throwable); 16 | throw throwable; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/bookstoread/Progress.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | public class Progress { 4 | 5 | 6 | private final int completed; 7 | private final int toRead; 8 | private final int inProgress; 9 | 10 | 11 | public Progress(int completed, int toRead, int inProgress) { 12 | this.completed = completed; 13 | this.toRead = toRead; 14 | this.inProgress = inProgress; 15 | } 16 | 17 | public static Progress notStarted() { 18 | return new Progress(0, 0, 0); 19 | } 20 | 21 | public int completed() { 22 | return this.completed; 23 | } 24 | 25 | public int toRead() { 26 | return this.toRead; 27 | } 28 | 29 | public int inProgress() { 30 | return this.inProgress; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /ivy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/BookShelfSpecWithRules.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.junit.Rule; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; 7 | import org.junit.rules.ExpectedException; 8 | 9 | import java.time.LocalDate; 10 | import java.time.Month; 11 | 12 | @EnableRuleMigrationSupport 13 | @ExtendWith(LoggingTestExecutionExceptionHandler.class) 14 | public class BookShelfSpecWithRules { 15 | 16 | @Rule 17 | public ExpectedException expectedException = ExpectedException.none(); 18 | Book effectiveJava = new Book("Effective Java", "Joshua Bloch", LocalDate.of(2008, Month.MAY, 8)); 19 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 20 | 21 | @Test 22 | void throwsExceptionWhenBooksAreAddedAfterCapacityIsReached() { 23 | BookShelf bookShelf = new BookShelf(1); 24 | expectedException.expect(BookShelfCapacityReached.class); 25 | expectedException.expectMessage("BookShelf capacity of 1 is reached. You can't add more books."); 26 | 27 | bookShelf.add(effectiveJava, codeComplete); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Freeware License, some rights reserved 2 | 3 | Copyright (c) 2017 Shekhar Gulati and Rahul Sharma 4 | 5 | Permission is hereby granted, free of charge, to anyone obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to work with the Software within the limits of freeware distribution and fair use. 8 | This includes the rights to use, copy, and modify the Software for personal use. 9 | Users are also allowed and encouraged to submit corrections and modifications 10 | to the Software for the benefit of other users. 11 | 12 | It is not allowed to reuse, modify, or redistribute the Software for 13 | commercial use in any way, or for a user’s educational materials such as books 14 | or blog articles without prior permission from the copyright holder. 15 | 16 | The above copyright notice and this permission notice need to be included 17 | in all copies or substantial portions of the software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS OR APRESS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/bookstoread/BookFilter.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import java.time.LocalDate; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.function.Function; 7 | 8 | public interface BookFilter { 9 | boolean apply(Book b); 10 | } 11 | 12 | class BookPublishedYearFilter implements BookFilter { 13 | private Function comparison; 14 | 15 | static BookPublishedYearFilter After(int year) { 16 | final LocalDate date = LocalDate.of(year, 12, 31); 17 | BookPublishedYearFilter filter = new BookPublishedYearFilter(); 18 | filter.comparison = date::isBefore; 19 | return filter; 20 | } 21 | 22 | static BookPublishedYearFilter Before(int year) { 23 | final LocalDate date = LocalDate.of(year, 1, 1); 24 | BookPublishedYearFilter filter = new BookPublishedYearFilter(); 25 | filter.comparison = date::isAfter; 26 | return filter; 27 | } 28 | 29 | @Override 30 | public boolean apply(final Book b) { 31 | return b!=null && comparison.apply(b.getPublishedOn()); 32 | } 33 | } 34 | 35 | class CompositeFilter implements BookFilter { 36 | private List filters; 37 | 38 | CompositeFilter() { 39 | filters = new ArrayList<>(); 40 | } 41 | 42 | @Override 43 | public boolean apply(final Book b) { 44 | return filters.stream() 45 | .map(bookFilter -> bookFilter.apply(b)) 46 | .reduce(true, (b1, b2) -> b1 && b2); 47 | } 48 | 49 | void addFilter(final BookFilter bookFilter) { 50 | filters.add(bookFilter); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/DynamicSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import java.time.DayOfWeek; 4 | import java.time.LocalDate; 5 | import java.time.Month; 6 | import java.time.format.DateTimeFormatter; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.Iterator; 10 | import java.util.stream.Stream; 11 | 12 | import org.junit.jupiter.api.DynamicTest; 13 | import org.junit.jupiter.api.TestFactory; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.junit.jupiter.params.ParameterizedTest; 16 | import org.junit.jupiter.params.provider.EnumSource; 17 | import org.junit.jupiter.params.provider.ValueSource; 18 | 19 | import static org.junit.jupiter.api.Assertions.*; 20 | import static org.junit.jupiter.api.Assertions.assertNotNull; 21 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; 22 | import static org.junit.jupiter.api.DynamicTest.stream; 23 | 24 | public class DynamicSpec { 25 | 26 | @TestFactory 27 | Collection generateFirstTest() { 28 | return Arrays.asList( 29 | dynamicTest("Week Test", () -> assertEquals(DayOfWeek.MONDAY,DayOfWeek.of(1))), 30 | dynamicTest("Month Test", () -> assertEquals(Month.JANUARY, Month.of(1))) 31 | ); 32 | } 33 | 34 | @TestFactory 35 | Stream generateParameterizedTest() { 36 | LocalDate startDate = LocalDate.now(); 37 | Iterator daysIter = Stream.iterate(startDate, date -> date.plusDays(1)).limit(10).iterator(); 38 | return stream(daysIter, d -> DateTimeFormatter.ISO_LOCAL_DATE.format(d), d -> assertNotNull(d)); 39 | } 40 | 41 | @TestFactory 42 | @ExtendWith(BooksProvider.class) 43 | Stream generateBooksTest(Book[] books) { 44 | return stream(Arrays.asList(books).iterator(), b -> String.format("Validating : %s",b.getTitle()), b -> assertFalse(b.isProgress())); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/workspace.xml 8 | .idea/tasks.xml 9 | .idea/dictionaries 10 | .idea/vcs.xml 11 | .idea/jsLibraryMappings.xml 12 | 13 | # Sensitive or high-churn files: 14 | .idea/dataSources.ids 15 | .idea/dataSources.xml 16 | .idea/dataSources.local.xml 17 | .idea/sqlDataSources.xml 18 | .idea/dynamic.xml 19 | .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | .idea/gradle.xml 23 | .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.iws 30 | 31 | ## Plugin-specific files: 32 | 33 | # IntelliJ 34 | /out/ 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | ### Gradle template 48 | .gradle 49 | build/ 50 | 51 | # Ignore Gradle GUI config 52 | gradle-app.setting 53 | 54 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 55 | !gradle-wrapper.jar 56 | 57 | # Cache of project 58 | .gradletasknamecache 59 | 60 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 61 | # gradle/wrapper/gradle-wrapper.properties 62 | ### Java template 63 | *.class 64 | 65 | # Mobile Tools for Java (J2ME) 66 | .mtj.tmp/ 67 | 68 | # Package Files # 69 | *.jar 70 | *.war 71 | *.ear 72 | 73 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 74 | hs_err_pid* 75 | 76 | .idea/ 77 | 78 | !gradle/wrapper/gradle-wrapper.jar 79 | 80 | target/ 81 | lib/ 82 | -------------------------------------------------------------------------------- /src/main/java/bookstoread/Book.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class Book implements Comparable { 6 | private final String title; 7 | private final String author; 8 | private final LocalDate publishedOn; 9 | private LocalDate startedReadingOn; 10 | private LocalDate finishedReadingOn; 11 | 12 | public Book(String title, String author, LocalDate publishedOn) { 13 | this.title = title; 14 | this.author = author; 15 | this.publishedOn = publishedOn; 16 | } 17 | 18 | public String getTitle() { 19 | return title; 20 | } 21 | 22 | public String getAuthor() { 23 | return author; 24 | } 25 | 26 | public LocalDate getPublishedOn() { 27 | return publishedOn; 28 | } 29 | 30 | public boolean isRead() { 31 | return startedReadingOn != null && finishedReadingOn != null; 32 | } 33 | 34 | public boolean isProgress() { 35 | return startedReadingOn != null && finishedReadingOn == null; 36 | } 37 | 38 | public void startedReadingOn(LocalDate startedOn) { 39 | this.startedReadingOn = startedOn; 40 | } 41 | 42 | public void finishedReadingOn(LocalDate finishedOn) { 43 | this.finishedReadingOn = finishedOn; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | if (o == null || getClass() != o.getClass()) return false; 50 | 51 | Book book = (Book) o; 52 | 53 | if (title != null ? !title.equals(book.title) : book.title != null) return false; 54 | if (author != null ? !author.equals(book.author) : book.author != null) return false; 55 | return publishedOn != null ? publishedOn.equals(book.publishedOn) : book.publishedOn == null; 56 | 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | int result = title != null ? title.hashCode() : 0; 62 | result = 31 * result + (author != null ? author.hashCode() : 0); 63 | result = 31 * result + (publishedOn != null ? publishedOn.hashCode() : 0); 64 | return result; 65 | } 66 | 67 | @Override 68 | public int compareTo(Book that) { 69 | return this.title.compareTo(that.title); 70 | } 71 | 72 | @Override 73 | public String toString() { 74 | return "Book{" + 75 | "title='" + title + '\'' + 76 | ", author='" + author + '\'' + 77 | ", publishedOn=" + publishedOn + 78 | '}'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/java/bookstoread/BookShelf.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import java.time.Year; 4 | import java.util.*; 5 | import java.util.function.Function; 6 | 7 | import static java.util.stream.Collectors.groupingBy; 8 | import static java.util.stream.Collectors.toList; 9 | 10 | public class BookShelf { 11 | 12 | private final List books = new ArrayList<>(); 13 | private final int capacity; 14 | 15 | public BookShelf() { 16 | this.capacity = Integer.MAX_VALUE; 17 | } 18 | 19 | public BookShelf(int capacity) { 20 | this.capacity = capacity; 21 | } 22 | 23 | public List books() { 24 | return Collections.unmodifiableList(books); 25 | } 26 | 27 | public void add(Book... booksToAdd) throws BookShelfCapacityReached { 28 | Arrays.stream(booksToAdd).forEach(book -> { 29 | if (books.size() == capacity) { 30 | throw new BookShelfCapacityReached(String.format("BookShelf capacity of %d is reached. You can't add more books.", this.capacity)); 31 | } 32 | books.add(book); 33 | }); 34 | } 35 | 36 | public List arrange() { 37 | return arrange(Comparator.naturalOrder()); 38 | } 39 | 40 | public List arrange(Comparator comparator) { 41 | return books 42 | .stream() 43 | .sorted(comparator) 44 | .collect(toList()); 45 | } 46 | 47 | public Map> groupByPublicationYear() { 48 | return groupBy(book -> Year.of(book.getPublishedOn().getYear())); 49 | } 50 | 51 | public Map> groupBy(Function fx) { 52 | return books 53 | .stream() 54 | .collect(groupingBy(fx)); 55 | } 56 | 57 | public Progress progress() { 58 | if (books.isEmpty()) { 59 | return Progress.notStarted(); 60 | } 61 | int booksRead = Long.valueOf(books.stream().filter(Book::isRead).count()).intValue(); 62 | int booksInProgress = Long.valueOf(books.stream().filter(Book::isProgress).count()).intValue(); 63 | int booksToRead = books.size() - booksRead - booksInProgress; 64 | 65 | int percentageCompleted = booksRead * 100 / books.size(); 66 | int percentageToRead = booksToRead * 100 / books.size(); 67 | int percentageInProgress = booksInProgress * 100 / books.size(); 68 | 69 | return new Progress(percentageCompleted, percentageToRead, percentageInProgress); 70 | } 71 | 72 | public List findBooksByTitle(String title) { 73 | return findBooksByTitle(title, b -> true); 74 | } 75 | 76 | public List findBooksByTitle(String title, BookFilter filter) { 77 | return books.stream() 78 | .filter(b -> b.getTitle().toLowerCase().contains(title)) 79 | .filter(b -> filter.apply(b)) 80 | .collect(toList()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/BookFilterTemplateSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.assertj.core.util.Lists; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.TestTemplate; 7 | import org.junit.jupiter.api.extension.*; 8 | 9 | import java.time.LocalDate; 10 | import java.time.Month; 11 | import java.util.List; 12 | import java.util.stream.Stream; 13 | 14 | import static org.junit.jupiter.api.Assertions.*; 15 | 16 | class BookFilterTemplateSpec { 17 | 18 | @BeforeAll 19 | static void print(){ 20 | 21 | 22 | } 23 | 24 | @BeforeEach 25 | void print1(){ 26 | System.out.println("aaaaaa"); 27 | } 28 | 29 | @TestTemplate 30 | @ExtendWith(BookFilterTestInvocationContextProvider.class) 31 | void validateFilters(BookFilter filter, Book[] books) { 32 | assertNotNull(filter); 33 | assertFalse(filter.apply(books[0])); 34 | assertTrue(filter.apply(books[1])); 35 | } 36 | } 37 | 38 | class BookFilterTestInvocationContextProvider implements TestTemplateInvocationContextProvider { 39 | @Override 40 | public boolean supportsTestTemplate(ExtensionContext context) { 41 | return true; 42 | } 43 | 44 | @Override 45 | public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { 46 | Book cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2008, Month.AUGUST, 1)); 47 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 48 | return Stream.of(bookFilterTestContext("Before Filter", BookPublishedYearFilter.Before(2007), cleanCode, codeComplete), 49 | bookFilterTestContext("After Filter", BookPublishedYearFilter.After(2007), codeComplete, cleanCode)); 50 | } 51 | 52 | private TestTemplateInvocationContext bookFilterTestContext(String testName, BookFilter bookFilter, Book... array) { 53 | return new TestTemplateInvocationContext() { 54 | @Override 55 | public String getDisplayName(int invocationIndex) { 56 | return testName; 57 | } 58 | 59 | @Override 60 | public List getAdditionalExtensions() { 61 | return Lists.newArrayList(new TypedParameterResolver(bookFilter), new TypedParameterResolver(array)); 62 | } 63 | }; 64 | } 65 | } 66 | 67 | class TypedParameterResolver implements ParameterResolver { 68 | T data; 69 | 70 | TypedParameterResolver(T data) { 71 | this.data = data; 72 | } 73 | 74 | @Override 75 | public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { 76 | Class parameterClass = parameterContext.getParameter().getType(); 77 | return parameterClass.isInstance(data); 78 | } 79 | 80 | @Override 81 | public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { 82 | return data; 83 | } 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/ParameterizedBookShelfSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.assertj.core.util.Arrays; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.api.extension.ExtensionContext; 7 | import org.junit.jupiter.params.ParameterizedTest; 8 | import org.junit.jupiter.params.provider.*; 9 | 10 | import java.time.LocalDate; 11 | import java.time.Month; 12 | import java.util.List; 13 | import java.util.stream.Stream; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | class ParameterizedBookShelfSpec { 19 | 20 | @ParameterizedTest 21 | @ValueSource(strings = {"Effective Java", "Code Complete", "Clean Code"}) 22 | void shouldGiveBackBooksForTitle(String title) { 23 | BookShelf shelf = new BookShelf(); 24 | Book effectiveJava = new Book("Effective Java", "Joshua Bloch", LocalDate.of(2008, Month.MAY, 8)); 25 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 26 | Book mythicalManMonth = new Book("The Mythical Man-Month", "Frederick Phillips Brooks", LocalDate.of(1975, Month.JANUARY, 1)); 27 | Book cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2008, Month.AUGUST, 1)); 28 | shelf.add(effectiveJava, codeComplete, mythicalManMonth, cleanCode); 29 | List foundBooks = shelf.findBooksByTitle(title.toLowerCase()); 30 | assertNotNull(foundBooks); 31 | assertEquals(1,foundBooks.size()); 32 | foundBooks = shelf.findBooksByTitle(title.toUpperCase()); 33 | assertNotNull(foundBooks); 34 | assertEquals(0,foundBooks.size()); 35 | } 36 | 37 | @ParameterizedTest 38 | @MethodSource("bookFilterProvider") 39 | void validateFilterWithNullData(BookFilter filter) { 40 | assertThat(filter.apply(null)).isFalse(); 41 | } 42 | 43 | @ParameterizedTest 44 | @ArgumentsSource(BookFilterCompositeArgsProvider.class) 45 | void validateBookFilterWithBooks(BookFilter filter, Book[] books) { 46 | assertNotNull(filter); 47 | assertFalse(filter.apply(books[0])); 48 | assertTrue(filter.apply(books[1])); 49 | } 50 | 51 | @DisplayName("Filter validates a passing book") 52 | @ParameterizedTest(name = "{index} : Validating {1}") 53 | @ArgumentsSource(BeforeYearArgsProvider.class) 54 | @ArgumentsSource(AfterYearArgsProvider.class) 55 | void validateBookFilterWithBook(BookFilter filter, Book book) { 56 | assertNotNull(filter); 57 | assertTrue(filter.apply(book)); 58 | } 59 | 60 | static Stream bookFilterProvider() { 61 | return Stream.of(BookPublishedYearFilter.Before(2007), BookPublishedYearFilter.After(2007)); 62 | } 63 | } 64 | 65 | class BookFilterCompositeArgsProvider implements ArgumentsProvider { 66 | 67 | @Override 68 | public Stream provideArguments(ExtensionContext context) { 69 | Book cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2008, Month.AUGUST, 1)); 70 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 71 | return Stream.of(Arguments.of(BookPublishedYearFilter.Before(2007), Arrays.array(cleanCode, codeComplete)), 72 | Arguments.of(BookPublishedYearFilter.After(2007), Arrays.array(codeComplete, cleanCode))); 73 | 74 | } 75 | } 76 | class BeforeYearArgsProvider implements ArgumentsProvider { 77 | 78 | @Override 79 | public Stream provideArguments(ExtensionContext context) { 80 | Book cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2006, Month.AUGUST, 1)); 81 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 82 | return Stream.of(Arguments.of(BookPublishedYearFilter.Before(2007), cleanCode), 83 | Arguments.of(BookPublishedYearFilter.Before(2007), codeComplete)); 84 | 85 | } 86 | } 87 | class AfterYearArgsProvider implements ArgumentsProvider { 88 | 89 | @Override 90 | public Stream provideArguments(ExtensionContext context) { 91 | Book cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2009, Month.AUGUST, 1)); 92 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2008, Month.JUNE, 9)); 93 | return Stream.of(Arguments.of(BookPublishedYearFilter.After(2007), cleanCode), 94 | Arguments.of(BookPublishedYearFilter.After(2007), codeComplete)); 95 | 96 | } 97 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.junit5book 6 | bookstoread 7 | 1.0-SNAPSHOT 8 | 9 | 10 | UTF-8 11 | 1.8 12 | 1.8 13 | 5.0.0-RC2 14 | 1.0.0-RC2 15 | 16 | 17 | 18 | 19 | 20 | maven-surefire-plugin 21 | 2.19.1 22 | 23 | ${surefireArgLine} 24 | 25 | **/*Spec.java 26 | 27 | 30 | 31 | 32 | 33 | org.junit.platform 34 | junit-platform-surefire-provider 35 | ${junit.platform.version} 36 | 37 | 38 | org.junit.jupiter 39 | junit-jupiter-engine 40 | ${junit.jupiter.version} 41 | 42 | 43 | 44 | 45 | org.jacoco 46 | jacoco-maven-plugin 47 | 48 | 49 | pre-unit-test 50 | 51 | prepare-agent 52 | 53 | 54 | 55 | ${project.build.directory}/coverage-reports/jacoco-ut.exec 56 | 60 | surefireArgLine 61 | 62 | 63 | 64 | post-unit-test 65 | test 66 | 67 | report 68 | 69 | 70 | 71 | ${project.build.directory}/coverage-reports/jacoco-ut.exec 72 | 73 | ${project.reporting.outputDirectory}/jacoco-ut 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | org.junit.jupiter 84 | junit-jupiter-api 85 | ${junit.jupiter.version} 86 | test 87 | 88 | 89 | org.junit.jupiter 90 | junit-jupiter-migrationsupport 91 | ${junit.jupiter.version} 92 | test 93 | 94 | 95 | org.assertj 96 | assertj-core 97 | 3.5.2 98 | test 99 | 100 | 101 | mockito-core 102 | org.mockito 103 | 2.7.12 104 | test 105 | 106 | 107 | junit 108 | junit 109 | 4.12 110 | test 111 | 112 | 113 | org.junit.platform 114 | junit-platform-runner 115 | ${junit.platform.version} 116 | test 117 | 118 | 119 | org.junit.jupiter 120 | junit-jupiter-engine 121 | ${junit.jupiter.version} 122 | test 123 | 124 | 125 | 126 | 127 | 128 | 129 | org.jacoco 130 | jacoco-maven-plugin 131 | 132 | 133 | 134 | 135 | report 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/BookShelfProgressSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.junit.jupiter.api.*; 4 | import org.junit.jupiter.api.extension.*; 5 | 6 | import java.time.LocalDate; 7 | import java.time.Month; 8 | import java.util.Arrays; 9 | import java.util.Collection; 10 | 11 | import org.junit.platform.runner.JUnitPlatform; 12 | import org.junit.runner.RunWith; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertTrue; 17 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; 18 | 19 | /** 20 | * Inject books in tests using a new provider. 21 | */ 22 | @RunWith(JUnitPlatform.class) 23 | @DisplayName("progress") 24 | @ExtendWith(BooksProvider.class) 25 | class BookShelfProgressSpec { 26 | 27 | private BookShelf shelf; 28 | 29 | /** 30 | * We can inject test metadata using TestInfo object. this is a replacement for TestName rule in Junit4 31 | * Similar to this there is a TestReporter which can be used to write back junit report. 32 | * Both these objects do no require any extension and are inject by the Junit engine. for any other type use Extension. 33 | * 34 | */ 35 | @BeforeEach 36 | void setup(Book[] books, TestInfo info) { 37 | System.out.println(" Test info with display Name as "+ info.getDisplayName()); 38 | shelf = new BookShelf(); 39 | shelf.add(books); 40 | } 41 | 42 | /** 43 | * Using assumptions to make sure that we have the same instance of book 44 | * Now that the instances are same so the tests will start working 45 | */ 46 | @Test 47 | @DisplayName("is 40% completed and 60% to-read when two books read and 3 books not read yet") 48 | void progressWithCompletedAndToReadPercentages(Book[] books) { 49 | Assumptions.assumeTrue(shelf.books().stream().filter(b -> b == books[0]).count() == 1); 50 | Assumptions.assumeTrue(shelf.books().stream().filter(b -> b == books[1]).count() == 1); 51 | 52 | books[0].startedReadingOn(LocalDate.of(2016, Month.JULY, 1)); 53 | books[0].finishedReadingOn(LocalDate.of(2016, Month.JULY, 31)); 54 | 55 | books[1].startedReadingOn(LocalDate.of(2016, Month.AUGUST, 1)); 56 | books[1].finishedReadingOn(LocalDate.of(2016, Month.AUGUST, 31)); 57 | 58 | Progress progress = shelf.progress(); 59 | 60 | assertThat(progress.completed()).isEqualTo(40); 61 | assertThat(progress.toRead()).isEqualTo(60); 62 | } 63 | 64 | @Test 65 | @DisplayName("is 0% completed and 100% to-read when no book is read yet") 66 | void progress100PercentUnread() { 67 | Progress progress = shelf.progress(); 68 | assertThat(progress.completed()).isEqualTo(0); 69 | assertThat(progress.toRead()).isEqualTo(100); 70 | } 71 | 72 | /** 73 | * Using assumptions to make sure that we have the same instance of book 74 | * Now that the instances are same so the tests will start working 75 | */ 76 | @Test 77 | @DisplayName("is 40% completed, 20% in-progress, and 40% to-read when 2 books read, 1 book in progress, and 2 books unread") 78 | void reportProgressOfCurrentlyReadingBooks(Book[] books) { 79 | Assumptions.assumeTrue(shelf.books().stream().filter(b -> b == books[0]).count() == 1); 80 | Assumptions.assumeTrue(shelf.books().stream().filter(b -> b == books[1]).count() == 1); 81 | Assumptions.assumeTrue(shelf.books().stream().filter(b -> b == books[2]).count() == 1); 82 | 83 | books[0].startedReadingOn(LocalDate.of(2016, Month.JULY, 1)); 84 | books[0].finishedReadingOn(LocalDate.of(2016, Month.JULY, 31)); 85 | 86 | books[1].startedReadingOn(LocalDate.of(2016, Month.AUGUST, 1)); 87 | books[1].finishedReadingOn(LocalDate.of(2016, Month.AUGUST, 31)); 88 | 89 | books[2].startedReadingOn(LocalDate.of(2016, Month.SEPTEMBER, 1)); 90 | 91 | Progress progress = shelf.progress(); 92 | 93 | assertThat(progress.completed()).isEqualTo(40); 94 | assertThat(progress.inProgress()).isEqualTo(20); 95 | assertThat(progress.toRead()).isEqualTo(40); 96 | } 97 | 98 | } 99 | 100 | class BooksProvider implements ParameterResolver { 101 | 102 | @Override 103 | public boolean supportsParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException { 104 | return parameterContext.getParameter().getType().equals(Book[].class); 105 | } 106 | 107 | /** 108 | * The method will create a new array every time. This is not a desired behaviour 109 | * in a test case we have asked books multiple times i.e dbefore each and the test method and both arrays will be different. 110 | * This cause test case failures 111 | * 112 | * FIX : the execution context can be used to create a store which can hold values to be used multiple times in the test case. 113 | * The change injects same books array when asked multiple times in a test execution 114 | */ 115 | @Override 116 | public Object resolveParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException { 117 | ExtensionContext.Store books = extensionContext.getStore(ExtensionContext.Namespace.create(Book.class)); 118 | return books.getOrComputeIfAbsent("booksList", key -> getBooks()); 119 | } 120 | 121 | Book[] getBooks(){ 122 | Book effectiveJava = new Book("Effective Java", "Joshua Bloch", LocalDate.of(2008, Month.MAY, 8)); 123 | Book codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 124 | Book mythicalManMonth = new Book("The Mythical Man-Month", "Frederick Phillips Brooks", LocalDate.of(1975, Month.JANUARY, 1)); 125 | Book cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2008, Month.AUGUST, 1)); 126 | Book refactoring = new Book("Refactoring: Improving the Design of Existing Code", "Martin Fowler", LocalDate.of(2002, Month.MARCH, 9)); 127 | return new Book[]{effectiveJava, codeComplete, mythicalManMonth, cleanCode, refactoring}; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/BookFilterSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | 4 | import java.time.LocalDate; 5 | import java.time.Month; 6 | import java.util.Arrays; 7 | import java.util.Collection; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import org.assertj.core.api.BooleanArrayAssert; 12 | import org.junit.jupiter.api.*; 13 | import org.junit.platform.runner.JUnitPlatform; 14 | import org.junit.runner.RunWith; 15 | import org.mockito.Mockito; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertFalse; 19 | import static org.junit.jupiter.api.Assertions.assertTrue; 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; 22 | 23 | interface FilterBoundaryTests { 24 | BookFilter get(); 25 | 26 | @Test 27 | @DisplayName("should not fail for null book.") 28 | default void nullTest() { 29 | assertThat(get().apply(null)).isFalse(); 30 | } 31 | 32 | } 33 | @RunWith(JUnitPlatform.class) 34 | @Tag("Filter") 35 | @DisplayName("Filter based on") 36 | class BookFilterSpec { 37 | private Book cleanCode; 38 | private Book codeComplete; 39 | 40 | @BeforeEach 41 | void init() { 42 | cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2008, Month.AUGUST, 1)); 43 | codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 44 | } 45 | 46 | @Nested 47 | @DisplayName("book published date post specified year") 48 | class BookPublishedAfterFilterSpec implements FilterBoundaryTests { 49 | BookFilter filter; 50 | 51 | @BeforeEach 52 | void init() { 53 | filter = BookPublishedYearFilter.After(2007); 54 | } 55 | 56 | @Override 57 | public BookFilter get() { 58 | return filter; 59 | } 60 | 61 | @Test 62 | @DisplayName("should give matching book") 63 | void validateBookPublishedDatePostAskedYear() { 64 | assertTrue(filter.apply(cleanCode)); 65 | assertFalse(filter.apply(codeComplete)); 66 | } 67 | } 68 | 69 | @Nested 70 | @DisplayName("book published date pre specified year") 71 | class BookPublishedBeforeFilterSpec implements FilterBoundaryTests { 72 | 73 | BookFilter filter; 74 | 75 | @BeforeEach 76 | void init() { 77 | filter = BookPublishedYearFilter.Before(2007); 78 | } 79 | 80 | @Override 81 | public BookFilter get() { 82 | return filter; 83 | } 84 | 85 | @Test 86 | @DisplayName("should give matching book") 87 | void validateBookPublishedDatePreAskedYear() { 88 | assertFalse(filter.apply(cleanCode)); 89 | assertTrue(filter.apply(codeComplete)); 90 | } 91 | } 92 | 93 | /** 94 | * can we really say that we have called all the filters here ? 95 | * Enters Mocking now ! 96 | */ 97 | @Test 98 | @DisplayName("Composite criteria invokes multiple filters") 99 | void shouldFilterOnMultiplesCriteria() { 100 | BookFilter mockedFilter = Mockito.mock(BookFilter.class); 101 | Mockito.when(mockedFilter.apply(cleanCode)).thenReturn(true); 102 | CompositeFilter compositeFilter = new CompositeFilter(); 103 | compositeFilter.addFilter(mockedFilter); 104 | compositeFilter.apply(cleanCode); 105 | Mockito.verify(mockedFilter).apply(cleanCode); 106 | } 107 | 108 | @Test 109 | @DisplayName("Composite criteria invokes all incase of failure") 110 | void shouldInvokeAllInFailure() { 111 | CompositeFilter compositeFilter = new CompositeFilter(); 112 | 113 | BookFilter invokedMockedFilter = Mockito.mock(BookFilter.class); 114 | Mockito.when(invokedMockedFilter.apply(cleanCode)).thenReturn(false); 115 | compositeFilter.addFilter(invokedMockedFilter); 116 | 117 | BookFilter secondInvokedMockedFilter = Mockito.mock(BookFilter.class); 118 | Mockito.when(secondInvokedMockedFilter.apply(cleanCode)).thenReturn(true); 119 | compositeFilter.addFilter(secondInvokedMockedFilter); 120 | 121 | assertFalse(compositeFilter.apply(cleanCode)); 122 | Mockito.verify(invokedMockedFilter).apply(cleanCode); 123 | Mockito.verify(secondInvokedMockedFilter).apply(cleanCode); 124 | } 125 | 126 | @Test 127 | @DisplayName("Composite criteria invokes all filters") 128 | void shouldInvokeAllFilters() { 129 | CompositeFilter compositeFilter = new CompositeFilter(); 130 | BookFilter firstInvokedMockedFilter = Mockito.mock(BookFilter.class); 131 | Mockito.when(firstInvokedMockedFilter.apply(cleanCode)).thenReturn(true); 132 | compositeFilter.addFilter(firstInvokedMockedFilter); 133 | 134 | BookFilter secondInvokedMockedFilter = Mockito.mock(BookFilter.class); 135 | Mockito.when(secondInvokedMockedFilter.apply(cleanCode)).thenReturn(true); 136 | compositeFilter.addFilter(secondInvokedMockedFilter); 137 | assertTrue(compositeFilter.apply(cleanCode)); 138 | Mockito.verify(firstInvokedMockedFilter).apply(cleanCode); 139 | Mockito.verify(secondInvokedMockedFilter).apply(cleanCode); 140 | } 141 | 142 | class MockedFilter implements BookFilter { 143 | boolean returnValue; 144 | boolean invoked; 145 | 146 | MockedFilter(boolean returnValue) { 147 | this.returnValue = returnValue; 148 | } 149 | 150 | @Override 151 | public boolean apply(Book b) { 152 | invoked = true; 153 | return returnValue; 154 | } 155 | } 156 | 157 | @TestFactory 158 | Collection dynamicTestsFromCollection() { 159 | BookFilter filter= null; 160 | return Arrays.asList( 161 | dynamicTest("1st dynamic test", () ->{ 162 | assertTrue(filter.apply(cleanCode)); 163 | assertFalse(filter.apply(codeComplete)); 164 | }), 165 | dynamicTest("2nd dynamic test", () ->{ 166 | assertFalse(filter.apply(cleanCode)); 167 | assertTrue(filter.apply(codeComplete)); 168 | }) 169 | ); 170 | 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/test/java/bookstoread/BookShelfSpec.java: -------------------------------------------------------------------------------- 1 | package bookstoread; 2 | 3 | import org.junit.jupiter.api.*; 4 | import org.junit.platform.runner.JUnitPlatform; 5 | import org.junit.runner.RunWith; 6 | 7 | import java.time.Duration; 8 | import java.time.LocalDate; 9 | import java.time.Month; 10 | import java.time.Year; 11 | import java.time.temporal.ChronoUnit; 12 | import java.util.Arrays; 13 | import java.util.Comparator; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | import static java.util.Arrays.asList; 18 | import static java.util.Collections.singletonList; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.junit.jupiter.api.Assertions.*; 21 | 22 | @RunWith(JUnitPlatform.class) 23 | @DisplayName("A bookshelf") 24 | public class BookShelfSpec { 25 | 26 | private BookShelf shelf; 27 | private Book effectiveJava; 28 | private Book codeComplete; 29 | private Book mythicalManMonth; 30 | private Book cleanCode; 31 | 32 | @BeforeEach 33 | void init() { 34 | shelf = new BookShelf(); 35 | effectiveJava = new Book("Effective Java", "Joshua Bloch", LocalDate.of(2008, Month.MAY, 8)); 36 | codeComplete = new Book("Code Complete", "Steve McConnel", LocalDate.of(2004, Month.JUNE, 9)); 37 | mythicalManMonth = new Book("The Mythical Man-Month", "Frederick Phillips Brooks", LocalDate.of(1975, Month.JANUARY, 1)); 38 | cleanCode = new Book("Clean Code", "Robert C. Martin", LocalDate.of(2008, Month.AUGUST, 1)); 39 | } 40 | 41 | //*************** BTR-1 *********************// 42 | 43 | @Nested 44 | @DisplayName("is empty") 45 | class IsEmpty { 46 | 47 | @Test 48 | @DisplayName("when no book is added to it") 49 | public void emptyBookShelfWhenNoBookAdded() { 50 | List books = shelf.books(); 51 | assertTrue(books.isEmpty(), () -> "BookShelf should be empty"); 52 | } 53 | 54 | /* 55 | Book Notes: 56 | 1. Time for clean up. Adding BeforeEach method 57 | */ 58 | @Test 59 | @DisplayName("when add is called without books") 60 | void emptyBookShelfWhenAddIsCalledWithoutBooks() { 61 | shelf.add(); 62 | List books = shelf.books(); 63 | assertTrue(books.isEmpty(), () -> "BookShelf should be empty."); 64 | } 65 | 66 | } 67 | 68 | @Nested 69 | @DisplayName("after adding books") 70 | class BooksAreAdded { 71 | 72 | /* 73 | Book Notes: 74 | 1. As you write test you will become clear if you want this kind of API or not 75 | Here rather than just supporting String in add we can use var args as well. That will allow us to 76 | add multiple books in one go. 77 | 2. You can use non-public methods as test names 78 | */ 79 | @Test 80 | @DisplayName("contains two books") 81 | void bookshelfContainsTwoBooksWhenTwoBooksAdded() { 82 | shelf.add(effectiveJava, codeComplete); 83 | List books = shelf.books(); 84 | assertEquals(2, books.size(), () -> "BookShelf should have two books"); 85 | } 86 | 87 | @Test 88 | @DisplayName("returns an immutable books collection to client") 89 | void bookshelfIsImmutableForClient() { 90 | shelf.add(effectiveJava, codeComplete); 91 | List books = shelf.books(); 92 | try { 93 | books.add(mythicalManMonth); 94 | fail(() -> "Should not be able to add book to books"); 95 | } catch (Exception e) { 96 | assertTrue(e instanceof UnsupportedOperationException, () -> "BookShelf should throw UnsupportedOperationException"); 97 | } 98 | } 99 | 100 | } 101 | 102 | @Test 103 | void throwsExceptionWhenBooksAreAddedAfterCapacityIsReached() { 104 | BookShelf bookShelf = new BookShelf(2); 105 | bookShelf.add(effectiveJava, codeComplete); 106 | BookShelfCapacityReached throwException = assertThrows(BookShelfCapacityReached.class, () -> bookShelf.add(mythicalManMonth)); 107 | assertEquals("BookShelf capacity of 2 is reached. You can't add more books.", throwException.getMessage()); 108 | } 109 | 110 | @Test 111 | void test_should_complete_in_one_second() { 112 | // assertTimeout(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000)); 113 | // String message = assertTimeout(Duration.of(1, ChronoUnit.SECONDS), () -> "Hello, World!"); 114 | // assertEquals("Hello, World!", message); 115 | 116 | 117 | assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000)); 118 | } 119 | 120 | @RepeatedTest(value = 10, name = "i_am_a_repeated_test__{currentRepetition}/{totalRepetitions}") 121 | void i_am_a_repeated_test() { 122 | assertTrue(true); 123 | } 124 | 125 | //*************** BTR-2 *********************// 126 | 127 | @Nested 128 | @DisplayName("is arranged") 129 | class WhenArranged { 130 | 131 | @Test 132 | @DisplayName("lexicographically by book title") 133 | void bookshelfArrangedByBookTitle() { 134 | shelf.add(effectiveJava, codeComplete, mythicalManMonth); 135 | List books = shelf.arrange(); 136 | assertEquals(asList(codeComplete, effectiveJava, mythicalManMonth), books, () -> "Books in a bookshelf should be arranged lexicographically by book title"); 137 | } 138 | 139 | 140 | /* 141 | Book note: 142 | We started with book as just a String primitive. This makes it easy to start off. 143 | Now, we realized book will have other properties so we will need to think about Book as a model class. 144 | Before we move ahead, let's create Book model with only single property 145 | After refactoring you will get java.lang.ClassCastException: bookstoread.Book cannot be cast to java.lang.Comparable. 146 | First we will have to implement Comparable interface and then we will have to add equals and hashcode method 147 | */ 148 | @Test 149 | @DisplayName("by user provided criteria (by book title lexicographically descending)") 150 | void bookshelfArrangedByUserProvidedCriteria() { 151 | shelf.add(effectiveJava, codeComplete, mythicalManMonth); 152 | List books = shelf.arrange(Comparator.naturalOrder().reversed()); 153 | assertEquals( 154 | asList(mythicalManMonth, effectiveJava, codeComplete), 155 | books, 156 | () -> "Books in a bookshelf are arranged in descending order of book title"); 157 | } 158 | 159 | /* 160 | Book note: 161 | One thing that reader should note here is that business logic `Comparator.naturalOrder().reversed()` in the above example is in the test. 162 | Our API supports client to provide their own criteria. If we in future we discovered that this should be in bookshelf then we can just move it there. 163 | You discover production code in tests. 164 | */ 165 | 166 | /* 167 | Book note: 168 | We will extend the Book model to include few more fields. 169 | 170 | */ 171 | @Test 172 | @DisplayName("by book publication date in ascending order") 173 | void bookshelfArrangedByAnotherUserProvidedCriteria() { 174 | shelf.add(effectiveJava, codeComplete, mythicalManMonth); 175 | List books = shelf.arrange((b1, b2) -> b1.getPublishedOn().compareTo(b2.getPublishedOn())); 176 | assertEquals( 177 | asList(mythicalManMonth, codeComplete, effectiveJava), 178 | books, 179 | () -> "Books in a bookshelf are arranged by book publication date in ascending order"); 180 | } 181 | 182 | } 183 | 184 | 185 | @Nested 186 | @DisplayName("books are grouped by") 187 | class GroupBy { 188 | 189 | /* 190 | Book note: 191 | Exercise: Ask readers to arrange book by author name 192 | */ 193 | 194 | /* 195 | Book note: 196 | One common requirement that comes is to group books within bookshelf by criteria. 197 | For example, group books by their author or publication year. 198 | Here we will introduce AssertJ assertions 199 | */ 200 | 201 | @Test 202 | @DisplayName("publication year") 203 | void groupBooksInBookShelfByPublicationYear() { 204 | shelf.add(effectiveJava, codeComplete, mythicalManMonth, cleanCode); 205 | 206 | Map> booksByPublicationYear = shelf.groupByPublicationYear(); 207 | assertThat(booksByPublicationYear) 208 | .containsKey(Year.of(2008)) 209 | .containsValues(Arrays.asList(effectiveJava, cleanCode)); 210 | 211 | assertThat(booksByPublicationYear) 212 | .containsKey(Year.of(2004)) 213 | .containsValues(singletonList(codeComplete)); 214 | 215 | assertThat(booksByPublicationYear) 216 | .containsKey(Year.of(1975)) 217 | .containsValues(singletonList(mythicalManMonth)); 218 | } 219 | 220 | /* 221 | Book note: 222 | If you think more we can make our function generic by extracting out grouping function 223 | */ 224 | @Test 225 | @DisplayName("user provided criteria(group by author name)") 226 | void groupBooksInBookShelfByUserProvidedCriteria() { 227 | shelf.add(effectiveJava, codeComplete, mythicalManMonth, cleanCode); 228 | Map> booksByAuthor = shelf.groupBy(Book::getAuthor); 229 | 230 | assertThat(booksByAuthor) 231 | .containsKey("Joshua Bloch") 232 | .containsValues(singletonList(effectiveJava)); 233 | 234 | assertThat(booksByAuthor) 235 | .containsKey("Steve McConnel") 236 | .containsValues(singletonList(codeComplete)); 237 | 238 | assertThat(booksByAuthor) 239 | .containsKey("Frederick Phillips Brooks") 240 | .containsValues(singletonList(mythicalManMonth)); 241 | 242 | assertThat(booksByAuthor) 243 | .containsKey("Robert C. Martin") 244 | .containsValues(singletonList(cleanCode)); 245 | } 246 | 247 | } 248 | } --------------------------------------------------------------------------------