├── 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 | 
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 extends Arguments> 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 extends Arguments> 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 extends Arguments> 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 | }
--------------------------------------------------------------------------------