├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── README.md
├── ch1
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch1
│ │ ├── Estimate.java
│ │ ├── MainMethodJohnUsedToTest.java
│ │ ├── PlanningPoker.java
│ │ └── PlanningPokerByJohn.java
│ └── test
│ └── java
│ └── ch1
│ └── PlanningPokerTest.java
├── ch10
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch10
│ │ ├── CustomerType.java
│ │ └── Invoice.java
│ └── test
│ └── java
│ └── ch10
│ ├── InvoiceBuilder.java
│ └── InvoiceTest.java
├── ch2
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch2
│ │ ├── CartItem.java
│ │ ├── NumberUtils.java
│ │ ├── ShoppingCart.java
│ │ └── StringUtils.java
│ └── test
│ └── java
│ └── ch2
│ ├── NumberUtilsNonSystematicTest.java
│ ├── NumberUtilsTest.java
│ ├── ShoppingCartTest.java
│ ├── StringUtilsExplorationTest.java
│ └── StringUtilsTest.java
├── ch3
├── coverage.sh
├── mutation.sh
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch3
│ │ ├── Clumps.java
│ │ ├── CountWords.java
│ │ └── LeftPadUtils.java
│ └── test
│ └── java
│ └── ch3
│ ├── ClumpsOnlyStructuralTest.java
│ ├── CountWordsTest.java
│ └── LeftPadTest.java
├── ch4
├── pom.xml
└── src
│ └── main
│ └── java
│ └── ch4
│ └── TaxCalculator.java
├── ch5
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch5
│ │ ├── ArrayUtils.java
│ │ ├── Basket.java
│ │ ├── BasketSkeleton.java
│ │ ├── Book.java
│ │ ├── MathArrays.java
│ │ ├── PassingGrade.java
│ │ ├── Product.java
│ │ └── Triangle.java
│ └── test
│ └── java
│ └── ch5
│ ├── ArrayUtilsTest.java
│ ├── BasketPBTest.java
│ ├── BasketTest.java
│ ├── BookTest.java
│ ├── MathArraysPBTest.java
│ ├── PassingGradePBTest.java
│ └── TriangleTest.java
├── ch6
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch6
│ │ ├── arguments
│ │ ├── InvoiceToSapInvoiceConverter.java
│ │ ├── SAP.java
│ │ ├── SAPInvoiceSender.java
│ │ └── SapInvoice.java
│ │ ├── bookstore
│ │ ├── Book.java
│ │ ├── BookRepository.java
│ │ ├── BookStore.java
│ │ ├── BuyBookProcess.java
│ │ └── Overview.java
│ │ ├── christmas
│ │ ├── ChristmasDiscount.java
│ │ └── Clock.java
│ │ ├── exception
│ │ ├── SAP.java
│ │ ├── SAPException.java
│ │ ├── SAPInvoiceSender.java
│ │ └── SapInvoice.java
│ │ ├── mock
│ │ ├── SAP.java
│ │ └── SAPInvoiceSender.java
│ │ └── stub
│ │ ├── DatabaseConnection.java
│ │ ├── Invoice.java
│ │ ├── InvoiceFilter.java
│ │ ├── InvoiceFilterWithDatabase.java
│ │ └── IssuedInvoices.java
│ └── test
│ └── java
│ └── ch6
│ ├── arguments
│ └── SAPInvoiceSenderTest.java
│ ├── bookstore
│ └── BookStoreTest.java
│ ├── christmas
│ └── ChristmasDiscountTest.java
│ ├── exception
│ └── SAPInvoiceSenderTest.java
│ ├── mock
│ └── SAPInvoiceSenderTest.java
│ └── stub
│ ├── InvoiceFilterTest.java
│ └── InvoiceFilterWithDatabaseTest.java
├── ch7
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ ├── adapters
│ │ ├── DeliveryCenterRestApi.java
│ │ ├── SAPSoapWebService.java
│ │ ├── SMTPCustomerNotifier.java
│ │ └── ShoppingCartHibernateDao.java
│ │ ├── domain
│ │ ├── Installment.java
│ │ ├── InstallmentGenerator.java
│ │ ├── InstallmentRepository.java
│ │ ├── PaidShoppingCartsBatch.java
│ │ ├── ShoppingCart.java
│ │ └── VeryBadPaidShoppingCartsBatch.java
│ │ └── ports
│ │ ├── CustomerNotifier.java
│ │ ├── DeliveryCenter.java
│ │ ├── SAP.java
│ │ └── ShoppingCartRepository.java
│ └── test
│ └── java
│ └── ch7
│ ├── InstallmentGeneratorTest.java
│ └── PaidShoppingCartsBatchTest.java
├── ch8
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── ch8
│ │ └── RomanNumeralConverter.java
│ └── test
│ └── java
│ └── ch8
│ └── RomanNumeralConverterTest.java
├── ch9
├── pom.xml
├── spring-petclinic-2.5.0-SNAPSHOT.jar
└── src
│ ├── main
│ └── java
│ │ └── ch9
│ │ ├── large
│ │ ├── DeliveryPrice.java
│ │ ├── ExtraChargeForElectronics.java
│ │ ├── FinalPriceCalculator.java
│ │ ├── FinalPriceCalculatorFactory.java
│ │ ├── Item.java
│ │ ├── ItemType.java
│ │ ├── PriceOfItems.java
│ │ ├── PriceRule.java
│ │ └── ShoppingCart.java
│ │ └── sql
│ │ ├── Invoice.java
│ │ └── InvoiceDao.java
│ └── test
│ └── java
│ └── ch9
│ ├── large
│ ├── DeliveryPriceTest.java
│ ├── ExtraChargeForElectronicsTest.java
│ ├── FinalPriceCalculatorLargerTest.java
│ ├── FinalPriceCalculatorTest.java
│ └── PriceOfItemsTest.java
│ ├── sql
│ ├── InvoiceDaoIntegrationTest.java
│ └── SqlIntegrationTestBase.java
│ └── system
│ ├── FindOwnersFlowTest.java
│ ├── FirstSeleniumTest.java
│ ├── WebTests.java
│ └── pages
│ ├── AddOwnerInfo.java
│ ├── AddOwnerPage.java
│ ├── FindOwnersPage.java
│ ├── ListOfOwnersPage.java
│ ├── OwnerInfo.java
│ ├── OwnerInformationPage.java
│ └── PetClinicPageObject.java
└── intro-to-junit
├── pom.xml
└── src
├── main
└── java
│ └── appendix
│ └── BlockCounter.java
└── test
└── java
└── appendix
├── BlockCounterParameterizedTest.java
├── BlockCounterParameterizedTest2.java
├── BlockCounterTest.java
└── BlockCounterWithBeforeAndAfterTest.java
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on: push
3 | jobs:
4 | ch1:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - name: Checkout the repository
8 | uses: actions/checkout@v2
9 | - name: Set up JDK 11
10 | uses: actions/setup-java@v1
11 | with:
12 | java-version: 11
13 | - name: Cache Maven packages
14 | uses: actions/cache@v2
15 | with:
16 | path: ~/.m2
17 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
18 | restore-keys: ${{ runner.os }}-m2
19 | - name: Run tests with Maven
20 | run: cd ch1 && mvn -B test --file pom.xml
21 | ch2:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - name: Checkout the repository
25 | uses: actions/checkout@v2
26 | - name: Set up JDK 11
27 | uses: actions/setup-java@v1
28 | with:
29 | java-version: 11
30 | - name: Cache Maven packages
31 | uses: actions/cache@v2
32 | with:
33 | path: ~/.m2
34 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
35 | restore-keys: ${{ runner.os }}-m2
36 | - name: Run tests with Maven
37 | run: cd ch2 && mvn -B test --file pom.xml
38 | ch3:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Checkout the repository
42 | uses: actions/checkout@v2
43 | - name: Set up JDK 11
44 | uses: actions/setup-java@v1
45 | with:
46 | java-version: 11
47 | - name: Cache Maven packages
48 | uses: actions/cache@v2
49 | with:
50 | path: ~/.m2
51 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
52 | restore-keys: ${{ runner.os }}-m2
53 | - name: Run tests with Maven
54 | run: cd ch3 && mvn -B test --file pom.xml
55 | ch4:
56 | runs-on: ubuntu-latest
57 | steps:
58 | - name: Checkout the repository
59 | uses: actions/checkout@v2
60 | - name: Set up JDK 11
61 | uses: actions/setup-java@v1
62 | with:
63 | java-version: 11
64 | - name: Cache Maven packages
65 | uses: actions/cache@v2
66 | with:
67 | path: ~/.m2
68 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
69 | restore-keys: ${{ runner.os }}-m2
70 | - name: Run tests with Maven
71 | run: cd ch4 && mvn -B test --file pom.xml
72 | ch5:
73 | runs-on: ubuntu-latest
74 | steps:
75 | - name: Checkout the repository
76 | uses: actions/checkout@v2
77 | - name: Set up JDK 11
78 | uses: actions/setup-java@v1
79 | with:
80 | java-version: 11
81 | - name: Cache Maven packages
82 | uses: actions/cache@v2
83 | with:
84 | path: ~/.m2
85 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
86 | restore-keys: ${{ runner.os }}-m2
87 | - name: Run tests with Maven
88 | run: cd ch5 && mvn -B test --file pom.xml
89 | ch6:
90 | runs-on: ubuntu-latest
91 | steps:
92 | - name: Checkout the repository
93 | uses: actions/checkout@v2
94 | - name: Set up JDK 11
95 | uses: actions/setup-java@v1
96 | with:
97 | java-version: 11
98 | - name: Cache Maven packages
99 | uses: actions/cache@v2
100 | with:
101 | path: ~/.m2
102 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
103 | restore-keys: ${{ runner.os }}-m2
104 | - name: Run tests with Maven
105 | run: cd ch6 && mvn -B test --file pom.xml
106 | ch7:
107 | runs-on: ubuntu-latest
108 | steps:
109 | - name: Checkout the repository
110 | uses: actions/checkout@v2
111 | - name: Set up JDK 11
112 | uses: actions/setup-java@v1
113 | with:
114 | java-version: 11
115 | - name: Cache Maven packages
116 | uses: actions/cache@v2
117 | with:
118 | path: ~/.m2
119 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
120 | restore-keys: ${{ runner.os }}-m2
121 | - name: Run tests with Maven
122 | run: cd ch7 && mvn -B test --file pom.xml
123 | ch8:
124 | runs-on: ubuntu-latest
125 | steps:
126 | - name: Checkout the repository
127 | uses: actions/checkout@v2
128 | - name: Set up JDK 11
129 | uses: actions/setup-java@v1
130 | with:
131 | java-version: 11
132 | - name: Cache Maven packages
133 | uses: actions/cache@v2
134 | with:
135 | path: ~/.m2
136 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
137 | restore-keys: ${{ runner.os }}-m2
138 | - name: Run tests with Maven
139 | run: cd ch8 && mvn -B test --file pom.xml
140 | ch9:
141 | runs-on: ubuntu-latest
142 | steps:
143 | - name: Checkout the repository
144 | uses: actions/checkout@v2
145 | - name: Set up JDK 11
146 | uses: actions/setup-java@v1
147 | with:
148 | java-version: 11
149 | - name: Cache Maven packages
150 | uses: actions/cache@v2
151 | with:
152 | path: ~/.m2
153 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
154 | restore-keys: ${{ runner.os }}-m2
155 | - name: Run tests with Maven
156 | run: cd ch9 && mvn -B test --file pom.xml
157 | ch10:
158 | runs-on: ubuntu-latest
159 | steps:
160 | - name: Checkout the repository
161 | uses: actions/checkout@v2
162 | - name: Set up JDK 11
163 | uses: actions/setup-java@v1
164 | with:
165 | java-version: 11
166 | - name: Cache Maven packages
167 | uses: actions/cache@v2
168 | with:
169 | path: ~/.m2
170 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
171 | restore-keys: ${{ runner.os }}-m2
172 | - name: Run tests with Maven
173 | run: cd ch10 && mvn -B test --file pom.xml
174 |
175 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
3 | target/
4 |
5 | *.iml
6 |
7 | .jqwik-database
8 |
9 | .DS_Store
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Effective software testing
2 |
3 | 
4 |
5 | This repository contains the code examples of the _Software Testing: A Developer's Guide_ book, by [Maurício Aniche](https://www.mauricioaniche.com).
6 |
7 | Each folder contains the code examples of their respective chapter:
8 |
9 | * Chapter 1: Effective and systematic software testing
10 | * Chapter 2: Specification-based testing
11 | * Chapter 3: Structural testing and code coverage
12 | * Chapter 4: Design by Contracts
13 | * Chapter 5: Property-based testing
14 | * Chapter 6: Test doubles and mocks
15 | * Chapter 7: Designing for testability
16 | * Chapter 8: Test-Driven Development
17 | * Chapter 9: Larger tests
18 | * Chapter 10: Test code quality
19 |
20 | Each folder is an independent maven project. You should be able to import the project directly in your favorite IDE (e.g., InteliiJ, Eclipse). You can also run all the tests via `mvn test`.
21 |
22 | To run code coverage in chapter 3, go to the ch3 folder and type `mvn clean test jacoco:report`. Then, open the `target/site/jacoco/index.html` file to see the report. If you want to run the mutation coverage, type `mvn clean compile test-compile pitest:mutationCoverage`. The report will be generated in the `target/pit-reports/**/index.html`, where `**` is a string that represents the date time that you ran the report. For Linux or Mac users, I provide bash scripts `coverage.sh` and `mutation.sh` that run the commands above for you.
23 |
24 | To run the web tests of chapter 9, you first should run the [Spring PetClinic](https://github.com/spring-projects/spring-petclinic) application. For convenience, we provide a compiled jar here. To run the web app, just go to the ch9 folder and type `java -jar *.jar`.
25 |
26 | ## Contributing to PRs
27 |
28 | Maybe you found a test I missed or a better way to implement the code. You are most welcome to submit your PRs!
29 |
30 | If you do so, I ask you to create another file, with the same name as the original plus some suffix, and add a comment explaining what you did there. I do not want to touch the original files as they match with the code snippets in the book; we do not want readers to get lost.
31 |
32 | ## License and reuse
33 |
34 | You are free to reuse and modify the code provided in this repository, for personal or business purposes, as long as the book is always explicitly mentioned as reference. For example, if you are providing training or workshops, you are required to have a dedicated slide with the picture of the book in each of the slide decks that make use of examples from here.
35 |
--------------------------------------------------------------------------------
/ch1/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch1
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.assertj
20 | assertj-core
21 | 3.15.0
22 | test
23 |
24 |
25 |
26 |
27 | org.junit.jupiter
28 | junit-jupiter-engine
29 | 5.6.2
30 | test
31 |
32 |
33 |
34 |
35 | net.jqwik
36 | jqwik
37 | 1.5.1
38 | test
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | maven-surefire-plugin
48 | 3.0.0-M5
49 |
50 | true
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/ch1/src/main/java/ch1/Estimate.java:
--------------------------------------------------------------------------------
1 | package ch1;
2 |
3 | import java.util.Objects;
4 |
5 | public class Estimate {
6 |
7 | private final String developer;
8 | private final int estimate;
9 |
10 | public Estimate(String developer, int estimate) {
11 | this.developer = developer;
12 | this.estimate = estimate;
13 | }
14 |
15 | public String getDeveloper() {
16 | return developer;
17 | }
18 |
19 | public int getEstimate() {
20 | return estimate;
21 | }
22 |
23 | @Override
24 | public String toString() {
25 | return "Estimate{" +
26 | "developer='" + developer + '\'' +
27 | ", estimate=" + estimate +
28 | '}';
29 | }
30 |
31 | @Override
32 | public boolean equals(Object o) {
33 | if (this == o) return true;
34 | if (o == null || getClass() != o.getClass()) return false;
35 | Estimate estimate1 = (Estimate) o;
36 | return estimate == estimate1.estimate && Objects.equals(developer, estimate1.developer);
37 | }
38 |
39 | @Override
40 | public int hashCode() {
41 | return Objects.hash(developer, estimate);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ch1/src/main/java/ch1/MainMethodJohnUsedToTest.java:
--------------------------------------------------------------------------------
1 | package ch1;
2 |
3 | import java.util.Arrays;
4 | import java.util.List;
5 |
6 | public class MainMethodJohnUsedToTest {
7 |
8 | public static void main(String[] args) {
9 | test1();
10 | test2();
11 | }
12 |
13 | private static void test1() {
14 | List developers = new PlanningPokerByJohn().identifyExtremes(
15 | Arrays.asList(
16 | new Estimate("Mauricio", 16),
17 | new Estimate("Frank", 8),
18 | new Estimate("Arie", 2),
19 | new Estimate("Andy", 4)));
20 |
21 | System.out.println(developers);
22 | }
23 |
24 | private static void test2() {
25 | List developers = new PlanningPokerByJohn().identifyExtremes(
26 | Arrays.asList(
27 | new Estimate("Ross", 2),
28 | new Estimate("Phoebe", 4),
29 | new Estimate("Monica", 8),
30 | new Estimate("Chandler", 16)));
31 |
32 | System.out.println(developers);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ch1/src/main/java/ch1/PlanningPoker.java:
--------------------------------------------------------------------------------
1 | package ch1;
2 |
3 | import java.util.Arrays;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | public class PlanningPoker {
8 |
9 | public List identifyExtremes(List estimates) {
10 |
11 | if(estimates == null) {
12 | throw new IllegalArgumentException("Estimates can't be null");
13 | }
14 | if(estimates.size() <= 1) {
15 | throw new IllegalArgumentException("There has to be more than 1 estimate in the list");
16 | }
17 |
18 | Estimate lowestEstimate = null;
19 | Estimate highestEstimate = null;
20 |
21 | for(Estimate estimate: estimates) {
22 | if(highestEstimate == null ||
23 | estimate.getEstimate() > highestEstimate.getEstimate()) {
24 | highestEstimate = estimate;
25 | }
26 | if(lowestEstimate == null ||
27 | estimate.getEstimate() < lowestEstimate.getEstimate()) {
28 | lowestEstimate = estimate;
29 | }
30 | }
31 |
32 | if(lowestEstimate.equals(highestEstimate))
33 | return Collections.emptyList();
34 |
35 | return Arrays.asList(
36 | lowestEstimate.getDeveloper(),
37 | highestEstimate.getDeveloper()
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ch1/src/main/java/ch1/PlanningPokerByJohn.java:
--------------------------------------------------------------------------------
1 | package ch1;
2 |
3 | import java.util.Arrays;
4 | import java.util.List;
5 |
6 | public class PlanningPokerByJohn {
7 | public List identifyExtremes(List estimates) {
8 |
9 | Estimate lowestEstimate = null;
10 | Estimate highestEstimate = null;
11 |
12 | for(Estimate estimate: estimates) {
13 | if(highestEstimate == null ||
14 | estimate.getEstimate() > highestEstimate.getEstimate()) {
15 | highestEstimate = estimate;
16 | }
17 | else if(lowestEstimate == null ||
18 | estimate.getEstimate() < lowestEstimate.getEstimate()) {
19 | lowestEstimate = estimate;
20 | }
21 | }
22 |
23 | return Arrays.asList(
24 | lowestEstimate.getDeveloper(),
25 | highestEstimate.getDeveloper()
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ch1/src/test/java/ch1/PlanningPokerTest.java:
--------------------------------------------------------------------------------
1 | package ch1;
2 |
3 | import net.jqwik.api.*;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.Arrays;
7 | import java.util.Collections;
8 | import java.util.List;
9 |
10 | import static org.assertj.core.api.Assertions.assertThat;
11 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
12 |
13 |
14 | public class PlanningPokerTest {
15 |
16 | @Test
17 | void rejectNullInput() {
18 | assertThatThrownBy(() -> new PlanningPoker().identifyExtremes(null))
19 | .isInstanceOf(IllegalArgumentException.class);
20 | }
21 |
22 | @Test
23 | void rejectEmptyList() {
24 | assertThatThrownBy(() -> {
25 | List emptyList = Collections.emptyList();
26 | new PlanningPoker().identifyExtremes(emptyList);
27 | }).isInstanceOf(IllegalArgumentException.class);
28 | }
29 |
30 | @Test
31 | void rejectSingleEstimate() {
32 | assertThatThrownBy(() -> {
33 | List list = Collections.singletonList(new Estimate("Eleanor", 1));
34 | new PlanningPoker().identifyExtremes(list);
35 | }).isInstanceOf(IllegalArgumentException.class);
36 | }
37 |
38 | @Test
39 | void twoEstimates() {
40 | List list = Arrays.asList(
41 | new Estimate("Mauricio", 10),
42 | new Estimate("Frank", 5)
43 | );
44 |
45 | List devs = new PlanningPoker().identifyExtremes(list);
46 |
47 | assertThat(devs)
48 | .containsExactlyInAnyOrder("Mauricio", "Frank");
49 | }
50 |
51 | // this test was later deleted by Eleanor, as the property based testing
52 | // replaces this one.
53 | @Test
54 | void manyEstimates() {
55 | List list = Arrays.asList(
56 | new Estimate("Mauricio", 10),
57 | new Estimate("Arie", 5),
58 | new Estimate("Frank", 7)
59 | );
60 |
61 | List devs = new PlanningPoker().identifyExtremes(list);
62 |
63 | assertThat(devs)
64 | .containsExactlyInAnyOrder("Mauricio", "Arie");
65 | }
66 |
67 | @Property
68 | void estimatesInAnyOrder(@ForAll("estimates") List estimates) {
69 |
70 | estimates.add(new Estimate("MrLowEstimate", 1));
71 | estimates.add(new Estimate("MsHighEstimate", 100));
72 | Collections.shuffle(estimates);
73 |
74 | List dev = new PlanningPoker().identifyExtremes(estimates);
75 |
76 | assertThat(dev)
77 | .containsExactlyInAnyOrder("MrLowEstimate", "MsHighEstimate");
78 | }
79 |
80 | @Provide
81 | Arbitrary> estimates() {
82 | Arbitrary names = Arbitraries.strings().withCharRange('a', 'z').ofLength(5);
83 | Arbitrary values = Arbitraries.integers().between(2, 99);
84 |
85 | Arbitrary estimates = Combinators.combine(names, values)
86 | .as((name, value) -> new Estimate(name, value));
87 |
88 | return estimates.list().ofMinSize(1);
89 | }
90 |
91 | @Test
92 | void developersWithSameEstimates() {
93 | List list = Arrays.asList(
94 | new Estimate("Mauricio", 10),
95 | new Estimate("Arie", 5),
96 | new Estimate("Andy", 10),
97 | new Estimate("Frank", 7),
98 | new Estimate("Annibale", 5)
99 | );
100 |
101 | List devs = new PlanningPoker().identifyExtremes(list);
102 |
103 | assertThat(devs)
104 | .containsExactlyInAnyOrder("Mauricio", "Arie");
105 | }
106 |
107 | @Test
108 | void allDevelopersWithTheSameEstimate() {
109 | List list = Arrays.asList(
110 | new Estimate("Mauricio", 10),
111 | new Estimate("Arie", 10),
112 | new Estimate("Andy", 10),
113 | new Estimate("Frank", 10),
114 | new Estimate("Annibale", 10)
115 | );
116 | List devs = new PlanningPoker().identifyExtremes(list);
117 |
118 | assertThat(devs).isEmpty();
119 |
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/ch10/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch10
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 |
20 | org.assertj
21 | assertj-core
22 | 3.15.0
23 | test
24 |
25 |
26 |
27 |
28 | org.junit.jupiter
29 | junit-jupiter-engine
30 | 5.6.2
31 | test
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | maven-surefire-plugin
41 | 3.0.0-M5
42 |
43 | true
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/ch10/src/main/java/ch10/CustomerType.java:
--------------------------------------------------------------------------------
1 | package ch10;
2 |
3 | public enum CustomerType {
4 | PERSON,
5 | COMPANY
6 | }
7 |
--------------------------------------------------------------------------------
/ch10/src/main/java/ch10/Invoice.java:
--------------------------------------------------------------------------------
1 | package ch10;
2 |
3 | public class Invoice {
4 |
5 | private final double value;
6 | private final String country;
7 | private final CustomerType customerType;
8 |
9 | public Invoice(double value, String country, CustomerType customerType) {
10 | this.value = value;
11 | this.country = country;
12 | this.customerType = customerType;
13 | }
14 |
15 | public double calculate() {
16 | double ratio = 0.1;
17 |
18 | // some business rule here to calculate the ratio
19 | // depending on the value, company/person, country ...
20 |
21 | return value * ratio;
22 | }
23 | }
--------------------------------------------------------------------------------
/ch10/src/test/java/ch10/InvoiceBuilder.java:
--------------------------------------------------------------------------------
1 | package ch10;
2 |
3 | public class InvoiceBuilder {
4 |
5 | private String country = "NL";
6 | private CustomerType customerType = CustomerType.PERSON;
7 | private double value = 500;
8 |
9 | public InvoiceBuilder withCountry(String country) {
10 | this.country = country;
11 | return this;
12 | }
13 |
14 | public InvoiceBuilder asCompany() {
15 | this.customerType = CustomerType.COMPANY;
16 | return this;
17 | }
18 |
19 | public InvoiceBuilder withAValueOf(double value) {
20 | this.value = value;
21 | return this;
22 | }
23 |
24 | public Invoice build() {
25 | return new Invoice(value, country, customerType);
26 | }
27 | }
--------------------------------------------------------------------------------
/ch10/src/test/java/ch10/InvoiceTest.java:
--------------------------------------------------------------------------------
1 | package ch10;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class InvoiceTest {
8 |
9 | @Test
10 | void taxesForCompanies() {
11 | Invoice invoice = new InvoiceBuilder()
12 | .asCompany()
13 | .withCountry("NL")
14 | .withAValueOf(2500.0)
15 | .build();
16 |
17 | double calculatedValue = invoice.calculate();
18 |
19 | assertThat(calculatedValue)
20 | .isEqualTo(250.0); // 2500 * 0.1 = 250
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ch2/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch2
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.assertj
20 | assertj-core
21 | 3.15.0
22 | test
23 |
24 |
25 |
26 |
27 | org.junit.jupiter
28 | junit-jupiter-engine
29 | 5.6.2
30 | test
31 |
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter-params
37 | 5.6.2
38 | test
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | maven-surefire-plugin
49 | 3.0.0-M5
50 |
51 | true
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/ch2/src/main/java/ch2/CartItem.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import java.util.Objects;
4 |
5 | public class CartItem {
6 |
7 | private final String product;
8 | private final int quantity;
9 | private final double unitPrice;
10 |
11 | public CartItem(String product, int quantity, double unitPrice) {
12 | this.product = product;
13 | this.quantity = quantity;
14 | this.unitPrice = unitPrice;
15 | }
16 |
17 | public String getProduct() {
18 | return product;
19 | }
20 |
21 | public int getQuantity() {
22 | return quantity;
23 | }
24 |
25 | public double getUnitPrice() {
26 | return unitPrice;
27 | }
28 |
29 | @Override
30 | public boolean equals(Object o) {
31 | if (this == o) return true;
32 | if (o == null || getClass() != o.getClass()) return false;
33 | CartItem cartItem = (CartItem) o;
34 | return quantity == cartItem.quantity && Double.compare(cartItem.unitPrice, unitPrice) == 0 && product.equals(cartItem.product);
35 | }
36 |
37 | @Override
38 | public int hashCode() {
39 | return Objects.hash(product, quantity, unitPrice);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ch2/src/main/java/ch2/NumberUtils.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import java.util.Collections;
4 | import java.util.LinkedList;
5 | import java.util.List;
6 |
7 | public class NumberUtils {
8 |
9 | /**
10 | * This method receives two numbers, `left` and `right`, both represented as a list of digits.
11 | * It adds these numbers and returns the result also as a list of digits.
12 | *
13 | * For example, if you want to add the numbers 23 and 42, you would need to create
14 | * a (left) list with two elements [2,3] and a (right) list with two elements [4,2].
15 | * 23+42 = 65, so the program would produce another list with two elements [6,5]
16 | *
17 | * [2,3] + [4,2] = [6,5]
18 | *
19 | * Each element in the left and right lists should be a number from [0-9].
20 | * An IllegalArgumentException is thrown in case this pre-condition does not hold.
21 | *
22 | * @param left a list containing the left number. Null returns null, empty means 0.
23 | * @param right a list containing the right number. Null returns null, empty means 0.
24 | * @return the sum of left and right, as a list
25 | */
26 | public static List add(List left, List right) {
27 | // if any is null, return null
28 | if (left == null || right == null)
29 | return null;
30 |
31 | // reverse the numbers so that the least significant digit goes to the left.
32 | Collections.reverse(left);
33 | Collections.reverse(right);
34 |
35 | LinkedList result = new LinkedList<>();
36 |
37 | // while there's a digit, keep summing them
38 | // if there's carry, take the carry into consideration
39 | int carry = 0;
40 | for (int i = 0; i < Math.max(left.size(), right.size()); i++) {
41 |
42 | int leftDigit = left.size() > i ? left.get(i) : 0;
43 | int rightDigit = right.size() > i ? right.get(i) : 0;
44 |
45 | if (leftDigit < 0 || leftDigit > 9 || rightDigit < 0 || rightDigit > 9)
46 | throw new IllegalArgumentException();
47 |
48 | int sum = leftDigit + rightDigit + carry;
49 |
50 | result.addFirst(sum % 10);
51 | carry = sum / 10;
52 | }
53 |
54 | // if there's some leftover carry, add it to the final number
55 | if (carry > 0)
56 | result.addFirst(carry);
57 |
58 | // remove leading zeroes from the result
59 | while (result.size() > 1 && result.get(0) == 0)
60 | result.remove(0);
61 |
62 | return result;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ch2/src/main/java/ch2/ShoppingCart.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | public class ShoppingCart {
7 |
8 | private List items = new ArrayList();
9 |
10 | public void add(CartItem item) {
11 | this.items.add(item);
12 | }
13 |
14 | public double totalPrice() {
15 | double totalPrice = 0;
16 | for (CartItem item : items) {
17 | totalPrice += item.getUnitPrice() * item.getQuantity();
18 | }
19 | return totalPrice;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ch2/src/main/java/ch2/StringUtils.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | public class StringUtils {
7 |
8 | private static final String[] EMPTY_STRING_ARRAY = new String[0];
9 |
10 | private static boolean isEmpty(final CharSequence cs) {
11 | return cs == null || cs.length() == 0;
12 | }
13 |
14 | /**
15 | * Searches a String for substrings delimited by a start and end tag,
16 | * returning all matching substrings in an array.
17 | *
18 | * @param str the String containing the substrings, null returns null, empty returns empty
19 | * @param open the String identifying the start of the substring, empty returns null
20 | * @param close the String identifying the end of the substring, empty returns null
21 | * @return a String Array of substrings, or {@code null} if no match
22 | */
23 | public static String[] substringsBetween(final String str, final String open, final String close) {
24 | if (str == null || isEmpty(open) || isEmpty(close)) {
25 | return null;
26 | }
27 | final int strLen = str.length();
28 | if (strLen == 0) {
29 | return EMPTY_STRING_ARRAY;
30 | }
31 | final int closeLen = close.length();
32 | final int openLen = open.length();
33 | final List list = new ArrayList<>();
34 | int pos = 0;
35 | while (pos < strLen - closeLen) {
36 | int start = str.indexOf(open, pos);
37 | if (start < 0) {
38 | break;
39 | }
40 | start += openLen;
41 | final int end = str.indexOf(close, start);
42 | if (end < 0) {
43 | break;
44 | }
45 | list.add(str.substring(start, end));
46 | pos = end + closeLen;
47 | }
48 | if (list.isEmpty()) {
49 | return null;
50 | }
51 | return list.toArray(EMPTY_STRING_ARRAY);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ch2/src/test/java/ch2/NumberUtilsNonSystematicTest.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 |
10 | public class NumberUtilsNonSystematicTest {
11 |
12 | @Test
13 | void t1() {
14 | assertThat(new NumberUtils().add(numbers(1), numbers(1)))
15 | .isEqualTo(numbers(2));
16 |
17 | assertThat(new NumberUtils().add(numbers(1,5), numbers(1,0)))
18 | .isEqualTo(numbers(2, 5));
19 |
20 | assertThat(new NumberUtils().add(numbers(1,5), numbers(1,5)))
21 | .isEqualTo(numbers(3,0));
22 |
23 | assertThat(new NumberUtils().add(numbers(5,0,0), numbers(2,5,0)))
24 | .isEqualTo(numbers(7,5,0));
25 | }
26 |
27 | private static List numbers(int... nums) {
28 | List list = new ArrayList<>();
29 | for(int n : nums)
30 | list.add(n);
31 | return list;
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/ch2/src/test/java/ch2/NumberUtilsTest.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.Arguments;
5 | import org.junit.jupiter.params.provider.MethodSource;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import java.util.stream.Stream;
10 |
11 | import static org.assertj.core.api.Assertions.assertThat;
12 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
13 | import static org.junit.jupiter.params.provider.Arguments.of;
14 |
15 | public class NumberUtilsTest {
16 |
17 | @ParameterizedTest
18 | @MethodSource("testCases")
19 | void shouldReturnCorrectResult(List left, List right, List expected) {
20 | assertThat(new NumberUtils().add(left, right))
21 | .isEqualTo(expected);
22 | }
23 |
24 | static Stream testCases() {
25 |
26 | /*
27 | * left:
28 | * - empty
29 | * - null
30 | * - single digit
31 | * - multiple digits
32 | * - zeroes on the left
33 | *
34 | * right:
35 | * - empty
36 | * - null
37 | * - single digit
38 | * - multiple digits
39 | * - zeroes on the left
40 | *
41 | * (left, right):
42 | * - len(left) > len(right)
43 | * - len(right) > len(left)
44 | * - len(left) = len(right)
45 | *
46 | * carry:
47 | * - sum without carry
48 | * - sum with carry
49 | * - one carry at the beginning
50 | * - one carry in the middle
51 | * - many carries
52 | * - many carries, not in a row
53 | * - carry in the last digit
54 | *
55 | * Test cases:
56 | *
57 | * T1 = left null
58 | * T2 = left empty
59 | * T3 = right null
60 | * T4 = right empty
61 | *
62 | * single digit:
63 | * T5 = single digit, no carry
64 | * T6 = single digit, carry
65 | *
66 | * multiple digits:
67 | * T7 = no carry
68 | * T8 = carry in the least significant digit
69 | * T9 = carry in the middle
70 | * T10 = many carries
71 | * T11 = many carries, not in a row
72 | * T12 = left over
73 | *
74 | * multiple digits, different length:
75 | * T13 = no carry
76 | * T14 = carry in the least significant digit
77 | * T15 = carry in the middle
78 | * T16 = many carries
79 | * T17 = many carries, not in a row
80 | * T18 = left over
81 | *
82 | * zeroes on the left:
83 | * T19 = no carry
84 | * T20 = carry
85 | * (do not see reason to combine with all the carries again)
86 | *
87 | * boundary:
88 | * T21 = carry to a new most significant digit, by one (e.g., 99+1).
89 | */
90 |
91 | return Stream.of(
92 | // nulls and empties should return null
93 | of(null, numbers(7,2), null), // T1
94 | of(numbers(), numbers(7,2), numbers(7,2)), // T2
95 | of(numbers(9,8), null, null), // T3
96 | of(numbers(9,8), numbers(), numbers(9,8)), // T4
97 |
98 | // single digits
99 | of(numbers(1), numbers(2), numbers(3)), // T5
100 | of(numbers(9), numbers(2), numbers(1,1)), // T6
101 |
102 | // multiple digits
103 | of(numbers(2,2), numbers(3,3), numbers(5,5)), // T7
104 | of(numbers(2,9), numbers(2,3), numbers(5,2)), // T8
105 | of(numbers(2,9,3), numbers(1,8,3), numbers(4,7,6)), // T9
106 | of(numbers(1,7,9), numbers(2,6,8), numbers(4,4,7)), // T10
107 | of(numbers(1,9,1,7,1), numbers(1,8,1,6,1), numbers(3,7,3,3,2)), // T11
108 | of(numbers(9,9,8), numbers(1,7,2), numbers(1,1,7,0)), // T12
109 |
110 | // multiple digits, different length, with and without carry
111 | // (from both sides)
112 | of(numbers(2,2), numbers(3), numbers(2,5)), // T13.1
113 | of(numbers(3), numbers(2,2), numbers(2,5)), // T13.2
114 |
115 | of(numbers(2,2), numbers(9), numbers(3,1)), // T14.1
116 | of(numbers(9), numbers(2,2), numbers(3,1)), // T14.2
117 |
118 | of(numbers(1,7,3), numbers(9,2), numbers(2,6,5)), // T15.1
119 | of(numbers(9,2), numbers(1,7,3), numbers(2,6,5)), // T15.2
120 |
121 | of(numbers(3,1,7,9), numbers(2,6,8), numbers(3,4,4,7)), // T16.1
122 | of(numbers(2,6,8), numbers(3,1,7,9), numbers(3,4,4,7)), // T16.2
123 |
124 | of(numbers(1,9,1,7,1), numbers(2,1,8,1,6,1), numbers(2,3,7,3,3,2)), // T17.1
125 | of(numbers(2,1,8,1,6,1), numbers(1,9,1,7,1), numbers(2,3,7,3,3,2)), // T17.2
126 |
127 | of(numbers(9,9,8), numbers(9,1,7,2), numbers(1,0,1,7,0)), // T18.1
128 | of(numbers(9,1,7,2), numbers(9,9,8), numbers(1,0,1,7,0)), // T18.2
129 |
130 | // zeroes on the left
131 | of(numbers(0,0,0,1,2), numbers(0,2,3), numbers(3,5)), // T19
132 | of(numbers(0,0,0,1,2), numbers(0,2,9), numbers(4,1)), // T20,
133 |
134 | // boundary
135 | of(numbers(9,9), numbers(1), numbers(1,0,0)) // T21
136 | );
137 | }
138 |
139 | @ParameterizedTest
140 | @MethodSource("digitsOutOfRange")
141 | void shouldThrowExceptionWhenDigitsAreOutOfRange(List left, List right) {
142 | assertThatThrownBy(() -> new NumberUtils().add(left, right))
143 | .isInstanceOf(IllegalArgumentException.class);
144 |
145 | }
146 |
147 | static Stream digitsOutOfRange() {
148 | return Stream.of(
149 | of(numbers(1,-1,1), numbers(1)),
150 | of(numbers(1), numbers(1,-1,1)),
151 | of(numbers(1,11,1), numbers(1)),
152 | of(numbers(1), numbers(1,11,1))
153 | );
154 | }
155 |
156 | // returns a mutable list of integers
157 | private static List numbers(int... nums) {
158 | List list = new ArrayList<>();
159 | for(int n : nums)
160 | list.add(n);
161 | return list;
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/ch2/src/test/java/ch2/ShoppingCartTest.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import static org.assertj.core.api.Assertions.assertThat;
5 |
6 | public class ShoppingCartTest {
7 | private final ShoppingCart cart = new ShoppingCart();
8 |
9 | @Test
10 | void noItems() {
11 | assertThat(cart.totalPrice()).isEqualTo(0);
12 | }
13 |
14 | @Test
15 | void itemsInTheCart() {
16 | cart.add(new CartItem("TV", 1, 120));
17 | assertThat(cart.totalPrice()).isEqualTo(120);
18 |
19 | cart.add(new CartItem("Chocolate", 2, 2.5));
20 | assertThat(cart.totalPrice()).isEqualTo(120 + 2.5*2);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ch2/src/test/java/ch2/StringUtilsExplorationTest.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | /**
8 | * These are the tests we made in the exploration phase (step 2)
9 | */
10 | public class StringUtilsExplorationTest {
11 |
12 | @Test
13 | void simpleCase() {
14 | assertThat(StringUtils.substringsBetween("abcd", "a", "d"))
15 | .isEqualTo(new String[] { "bc" });
16 | }
17 |
18 | @Test
19 | void manySubstrings() {
20 | assertThat(StringUtils.substringsBetween("abcdabcdab", "a", "d"))
21 | .isEqualTo(new String[] { "bc", "bc" });
22 | }
23 |
24 | @Test
25 | void openAndCloseTagsThatAreLongerThan1Char() {
26 | assertThat(StringUtils.substringsBetween("aabcddaabfddaab", "aa", "dd"))
27 | .isEqualTo(new String[] { "bc", "bf" });
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/ch2/src/test/java/ch2/StringUtilsTest.java:
--------------------------------------------------------------------------------
1 | package ch2;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import static ch2.StringUtils.substringsBetween;
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class StringUtilsTest {
8 |
9 | @Test void strIsNullOrEmpty() {
10 | assertThat(substringsBetween(null, "a", "b")).isEqualTo(null);
11 | assertThat(substringsBetween("", "a", "b")).isEqualTo(new String[]{});
12 | }
13 |
14 | @Test
15 | void openIsNullOrEmpty() {
16 | assertThat(substringsBetween("abc", null, "b")).isEqualTo(null);
17 | assertThat(substringsBetween("abc", "", "b")).isEqualTo(null);
18 | }
19 |
20 | @Test
21 | void closeIsNullOrEmpty() {
22 | assertThat(substringsBetween("abc", "a", null)).isEqualTo(null);
23 | assertThat(substringsBetween("abc", "a", null)).isEqualTo(null);
24 | }
25 |
26 | @Test
27 | void strOfLength1() {
28 | assertThat(substringsBetween("a", "a", "b")).isEqualTo(null);
29 | assertThat(substringsBetween("a", "b", "a")).isEqualTo(null);
30 | assertThat(substringsBetween("a", "b", "b")).isEqualTo(null);
31 | assertThat(substringsBetween("a", "a", "a")).isEqualTo(null);
32 | }
33 |
34 | @Test
35 | void openAndCloseOfLength1() {
36 | assertThat(substringsBetween("abc", "x", "y")).isEqualTo(null);
37 | assertThat(substringsBetween("abc", "a", "y")).isEqualTo(null);
38 | assertThat(substringsBetween("abc", "x", "c")).isEqualTo(null);
39 | assertThat(substringsBetween("abc", "a", "c")).isEqualTo(new String[] {"b"});
40 | assertThat(substringsBetween("abcabc", "a", "c")).isEqualTo(new String[] {"b", "b"});
41 | assertThat(substringsBetween("abcabyt byrc", "a", "c")).isEqualTo(new String[] {"b", "byt byr"});
42 | }
43 |
44 | @Test
45 | void openAndCloseTagsOfDifferentSizes() {
46 | assertThat(substringsBetween("aabcc", "xx", "yy")).isEqualTo(null);
47 | assertThat(substringsBetween("aabcc", "aa", "yy")).isEqualTo(null);
48 | assertThat(substringsBetween("aabcc", "xx", "cc")).isEqualTo(null);
49 | assertThat(substringsBetween("aabbcc", "aa", "cc")).isEqualTo(new String[] {"bb"});
50 | assertThat(substringsBetween("aabbccaaeecc", "aa", "cc")).isEqualTo(new String[] {"bb", "ee"});
51 | assertThat(substringsBetween("a abb ddc ca abbcc", "a a", "c c")).isEqualTo(new String[] {"bb dd"});
52 | }
53 |
54 | @Test
55 | void noSubstringBetweenOpenAndCloseTags() {
56 | assertThat(substringsBetween("aabb", "aa", "bb")).isEqualTo(new String[] {""});
57 | }
58 |
59 | @Test
60 | void closeTagAppearingMultipleTimes() {
61 | assertThat(substringsBetween("aabcddaabeddaab", "aa", "d")).isEqualTo(new String[] {"bc", "be"});
62 | assertThat(substringsBetween("aabcddabeddaab", "aa", "d")).isEqualTo(new String[] {"bc"});
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ch3/coverage.sh:
--------------------------------------------------------------------------------
1 | mvn clean test jacoco:report
2 | open target/site/jacoco/index.html
3 |
--------------------------------------------------------------------------------
/ch3/mutation.sh:
--------------------------------------------------------------------------------
1 | rm -rf target/pit-reports
2 | mvn clean compile test-compile pitest:mutationCoverage
3 | open target/pit-reports/**/index.html
4 |
--------------------------------------------------------------------------------
/ch3/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch3
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.assertj
20 | assertj-core
21 | 3.15.0
22 | test
23 |
24 |
25 |
26 |
27 | org.junit.jupiter
28 | junit-jupiter-engine
29 | 5.6.2
30 | test
31 |
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter-params
37 | 5.6.2
38 | test
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.jacoco
49 | jacoco-maven-plugin
50 | 0.8.7
51 |
52 |
53 |
54 | prepare-agent
55 |
56 |
57 |
58 | report
59 | test
60 |
61 | report
62 |
63 |
64 |
65 |
66 |
67 |
68 | maven-surefire-plugin
69 | 2.22.2
70 |
71 | true
72 |
73 |
74 |
75 |
76 | org.pitest
77 | pitest-maven
78 | 1.5.1
79 |
80 |
81 |
82 | org.pitest
83 | pitest-junit5-plugin
84 | 0.12
85 |
86 |
87 |
88 |
89 | ALL
90 |
91 |
92 | -ea
93 |
94 | false
95 |
96 | ch3.*
97 |
98 |
99 | ch3.*
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/ch3/src/main/java/ch3/Clumps.java:
--------------------------------------------------------------------------------
1 | package ch3;
2 |
3 | public class Clumps {
4 |
5 | /**
6 | * Counts the number of "clumps" that are in the array. A clump is a sequence of
7 | * the same element with a length of at least 2.
8 | *
9 | * @param nums
10 | * the array to count the clumps of. The array must be non-null and
11 | * len > 0; the program returns 0 in case any pre-condition is
12 | * violated.
13 | * @return the number of clumps in the array.
14 | */
15 | public static int countClumps(int[] nums) {
16 | if (nums == null || nums.length == 0) {
17 | return 0;
18 | }
19 | int count = 0;
20 | int prev = nums[0];
21 | boolean inClump = false;
22 | for (int i = 1; i < nums.length; i++) {
23 | if (nums[i] == prev && !inClump) {
24 | inClump = true;
25 | count += 1;
26 | }
27 | if (nums[i] != prev) {
28 | prev = nums[i];
29 | inClump = false;
30 | }
31 | }
32 | return count;
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/ch3/src/main/java/ch3/CountWords.java:
--------------------------------------------------------------------------------
1 | package ch3;
2 |
3 | public class CountWords {
4 | public int count(String str) {
5 | int words = 0;
6 | char last = ' ';
7 | for (int i = 0; i < str.length(); i++) {
8 | if (!Character.isLetter(str.charAt(i)) && (last == 's' || last == 'r')) {
9 | words++;
10 | }
11 | last = str.charAt(i);
12 | }
13 | if (last == 'r' || last == 's')
14 | words++;
15 | return words;
16 | }
17 | }
--------------------------------------------------------------------------------
/ch3/src/main/java/ch3/LeftPadUtils.java:
--------------------------------------------------------------------------------
1 | package ch3;
2 |
3 | public class LeftPadUtils {
4 |
5 | private static final String SPACE = " ";
6 |
7 | private static boolean isEmpty(final CharSequence cs) {
8 | return cs == null || cs.length() == 0;
9 | }
10 |
11 | /**
12 | * Left pad a String with a specified String.
13 | *
14 | * Pad to a size of {@code size}.
15 | *
16 | * @param str the String to pad out, may be null
17 | * @param size the size to pad to
18 | * @param padStr the String to pad with, null or empty treated as single space
19 | * @return left padded String or original String if no padding is necessary,
20 | * {@code null} if null String input
21 | */
22 | public static String leftPad(final String str, final int size, String padStr) {
23 | if (str == null) {
24 | return null;
25 | }
26 | if (isEmpty(padStr)) {
27 | padStr = SPACE;
28 | }
29 | final int padLen = padStr.length();
30 | final int strLen = str.length();
31 | final int pads = size - strLen;
32 | if (pads <= 0) {
33 | return str; // returns original String when possible
34 | }
35 |
36 | if (pads == padLen) {
37 | return padStr.concat(str);
38 | } else if (pads < padLen) {
39 | return padStr.substring(0, pads).concat(str);
40 | } else {
41 | final char[] padding = new char[pads];
42 | final char[] padChars = padStr.toCharArray();
43 | for (int i = 0; i < pads; i++) {
44 | padding[i] = padChars[i % padLen];
45 | }
46 | return new String(padding).concat(str);
47 | }
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/ch3/src/test/java/ch3/ClumpsOnlyStructuralTest.java:
--------------------------------------------------------------------------------
1 | package ch3;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.Arguments;
5 | import org.junit.jupiter.params.provider.MethodSource;
6 |
7 | import java.util.stream.Stream;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 | import static org.junit.jupiter.params.provider.Arguments.of;
11 |
12 | public class ClumpsOnlyStructuralTest {
13 |
14 | @ParameterizedTest
15 | @MethodSource("generator")
16 | void testClumps(int[] nums, int expectedNoOfClumps) {
17 | assertThat(Clumps.countClumps(nums))
18 | .isEqualTo(expectedNoOfClumps);
19 | }
20 |
21 | static Stream generator() {
22 | return Stream.of(
23 | of(new int[]{}, 0), // empty
24 | of(null, 0), // null
25 | of(new int[]{1,2,2,2,1}, 1), // one clump
26 | of(new int[]{1}, 0), // one element
27 |
28 | // an example of a missing test case!! Structural testing is not enough!
29 | of(new int[]{2,2}, 1)
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ch3/src/test/java/ch3/CountWordsTest.java:
--------------------------------------------------------------------------------
1 | package ch3;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | // this test suite is incomplete. Do proper specification-based testing here!
8 | public class CountWordsTest {
9 |
10 | @Test
11 | void t1() {
12 | int words = new CountWords().count("dogs cats");
13 | assertThat(words).isEqualTo(2);
14 | }
15 |
16 | @Test
17 | void t2() {
18 | int words = new CountWords().count("dog cat");
19 | assertThat(words).isEqualTo(0);
20 | }
21 |
22 | @Test
23 | void t3() {
24 | int words = new CountWords().count("car bar");
25 | assertThat(words).isEqualTo(2);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ch3/src/test/java/ch3/LeftPadTest.java:
--------------------------------------------------------------------------------
1 | package ch3;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.junit.jupiter.params.ParameterizedTest;
5 | import org.junit.jupiter.params.provider.Arguments;
6 | import org.junit.jupiter.params.provider.MethodSource;
7 |
8 | import java.util.stream.Stream;
9 |
10 | import static ch3.LeftPadUtils.leftPad;
11 | import static org.assertj.core.api.Assertions.assertThat;
12 | import static org.junit.jupiter.params.provider.Arguments.of;
13 |
14 | public class LeftPadTest {
15 |
16 | @ParameterizedTest
17 | @MethodSource("generator")
18 | void test(String originalStr, int size, String padString, String expectedStr) {
19 | assertThat(leftPad(originalStr, size, padString))
20 | .isEqualTo(expectedStr);
21 | }
22 |
23 | static Stream generator() {
24 | return Stream.of(
25 | of(null, 10, "-", null), // T1
26 | of("", 5, "-", "-----"), // T2
27 | of("abc", -1, "-", "abc"), // T3
28 | of("abc", 5, null, " abc"), // T4
29 | of("abc", 5, "", " abc"), // T5
30 | of("abc", 5, "-", "--abc"), // T6
31 | of("abc", 3, "-", "abc"), // T7
32 | of("abc", 0, "-", "abc"), // T8
33 | of("abc", 2, "-", "abc"), // T9
34 | of("abc", 5, "--", "--abc"), // T10
35 | of("abc", 5, "---", "--abc"), // T11
36 | of("abc", 5, "-", "--abc") // T12
37 | );
38 | }
39 |
40 | @Test
41 | void sameInstance() {
42 | String str = "sometext";
43 | assertThat(leftPad(str, 5, "-"))
44 | .isSameAs(str);
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/ch4/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch4
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.assertj
20 | assertj-core
21 | 3.15.0
22 | test
23 |
24 |
25 |
26 |
27 | org.junit.jupiter
28 | junit-jupiter-engine
29 | 5.6.2
30 | test
31 |
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter-params
37 | 5.6.2
38 | test
39 |
40 |
41 |
42 |
43 | net.jqwik
44 | jqwik
45 | 1.5.3
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | maven-surefire-plugin
57 | 3.0.0-M5
58 |
59 | true
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ch4/src/main/java/ch4/TaxCalculator.java:
--------------------------------------------------------------------------------
1 | package ch4;
2 |
3 | public class TaxCalculator {
4 | /**
5 | * Calculates the tax according to (some
6 | * explanation here...)
7 | *
8 | * @param value the base value for tax calculation. Value has
9 | * to be a positive number.
10 | * @return the calculated tax. The tax is always a positive number.
11 | */
12 | public double calculateTax(double value) {
13 | // pre-condition check
14 | if(value < 0) {
15 | throw new RuntimeException("Value has to be positive");
16 | }
17 |
18 | double taxValue = 0;
19 |
20 |
21 | // some complex business rule here...
22 | // final value goes to 'taxValue'
23 |
24 | // post-condition check
25 | if(taxValue < 0) {
26 | throw new RuntimeException("Calculated tax cannot be negative");
27 | }
28 |
29 | return taxValue;
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ch5/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch5
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.assertj
20 | assertj-core
21 | 3.15.0
22 | test
23 |
24 |
25 |
26 |
27 | org.junit.jupiter
28 | junit-jupiter-engine
29 | 5.6.2
30 | test
31 |
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter-params
37 | 5.6.2
38 | test
39 |
40 |
41 |
42 |
43 | net.jqwik
44 | jqwik
45 | 1.5.3
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | maven-surefire-plugin
57 | 3.0.0-M5
58 |
59 | true
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/ArrayUtils.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | class ArrayUtils {
4 |
5 | public static int indexOf(final int[] array, final int valueToFind, int startIndex) {
6 | if (array == null) {
7 | return -1;
8 | }
9 | if (startIndex < 0) {
10 | startIndex = 0;
11 | }
12 | for (int i = startIndex; i < array.length; i++) {
13 | if (valueToFind == array[i]) {
14 | return i;
15 | }
16 | }
17 | return -1;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/Basket.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import java.math.BigDecimal;
4 | import java.util.Collections;
5 | import java.util.HashMap;
6 | import java.util.Map;
7 | import java.util.Set;
8 |
9 | import static java.math.BigDecimal.valueOf;
10 |
11 | public class Basket {
12 | private BigDecimal totalValue = BigDecimal.ZERO;
13 | private Map basket = new HashMap<>();
14 |
15 | public void add(Product product, int qtyToAdd) {
16 | assert product != null : "Product is required";
17 | assert qtyToAdd > 0 : "Quantity has to be greater than zero";
18 | BigDecimal oldTotalValue = totalValue;
19 |
20 | int existingQuantity = basket.getOrDefault(product, 0);
21 | int newQuantity = existingQuantity + qtyToAdd;
22 | basket.put(product, newQuantity);
23 |
24 | BigDecimal valueAlreadyInTheCart = product.getPrice().multiply(valueOf(existingQuantity));
25 | BigDecimal newFinalValueForTheProduct = product.getPrice().multiply(valueOf(newQuantity));
26 |
27 | totalValue = totalValue
28 | .subtract(valueAlreadyInTheCart)
29 | .add(newFinalValueForTheProduct);
30 |
31 | assert basket.containsKey(product) : "Product was not inserted in the basket";
32 | assert totalValue.compareTo(oldTotalValue) == 1 : "Total value should be greater than previous total value";
33 | assert invariant() : "Invariant does not hold";
34 | }
35 |
36 | public void remove(Product product) {
37 | assert product != null : "product can't be null";
38 | assert basket.containsKey(product) : "Product must already be in the basket";
39 |
40 | int qty = basket.get(product);
41 |
42 | totalValue = totalValue.subtract(product.getPrice().multiply(valueOf(qty)));
43 |
44 | basket.remove(product);
45 |
46 | assert !basket.containsKey(product) : "Product is still in the basket";
47 | assert invariant() : "Invariant does not hold";
48 | }
49 |
50 | private boolean invariant() {
51 | return totalValue.compareTo(BigDecimal.ZERO) >= 0;
52 | }
53 |
54 | public BigDecimal getTotalValue() {
55 | return totalValue;
56 | }
57 |
58 | public int quantityOf(Product product) {
59 | assert basket.containsKey(product);
60 | return basket.get(product);
61 | }
62 |
63 | public Set products() {
64 | return Collections.unmodifiableSet(basket.keySet());
65 | }
66 |
67 | @Override
68 | public String toString() {
69 | return "BasketCase{" +
70 | "totalValue=" + totalValue +
71 | ", basket=" + basket +
72 | '}';
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/BasketSkeleton.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | public class BasketSkeleton {
7 | private double totalValue = 0.0;
8 | private Map basket = new HashMap<>();
9 |
10 | public void add(Product product, int qtyToAdd) {
11 | assert product != null : "Product is required";
12 | assert qtyToAdd > 0 : "Quantity has to be greater than zero";
13 | double oldTotalValue = totalValue;
14 |
15 | // add the product in the basket
16 | // update the total value
17 |
18 | assert basket.containsKey(product) : "Product was not inserted in the basket";
19 | assert totalValue > oldTotalValue : "Total value should be greater than previous total value";
20 | assert invariant() : "Invariant does not hold";
21 | }
22 |
23 | public void remove(Product product) {
24 | assert product != null : "product can't be null";
25 | assert basket.containsKey(product) : "Product must already be in the basket";
26 |
27 | // remove the product from the basket
28 | // update the total value
29 |
30 | assert !basket.containsKey(product) : "Product is still in the basket";
31 | assert invariant() : "Invariant does not hold";
32 | }
33 |
34 | private boolean invariant() {
35 | return totalValue >= 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/Book.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | public class Book {
4 |
5 | private final String title;
6 | private final String author;
7 | private final int qtyOfPages;
8 |
9 | public Book(String title, String author, int qtyOfPages) {
10 | this.title = title;
11 | this.author = author;
12 | this.qtyOfPages = qtyOfPages;
13 | }
14 |
15 | public String getTitle() {
16 | return title;
17 | }
18 |
19 | public String getAuthor() {
20 | return author;
21 | }
22 |
23 | public int getQtyOfPages() {
24 | return qtyOfPages;
25 | }
26 |
27 | @Override
28 | public String toString() {
29 | return "Book{" +
30 | "title='" + title + '\'' +
31 | ", author='" + author + '\'' +
32 | ", qtyOfPages=" + qtyOfPages +
33 | '}';
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/MathArrays.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import java.util.Iterator;
4 | import java.util.TreeSet;
5 |
6 | public class MathArrays {
7 |
8 | /**
9 | * Returns an array consisting of the unique values in {@code data}.
10 | * The return array is sorted in descending order. Empty arrays
11 | * are allowed, but null arrays result in NullPointerException.
12 | * Infinities are allowed. NaN values are allowed with maximum
13 | * sort order - i.e., if there are NaN values in {@code data},
14 | * {@code Double.NaN} will be the first element of the output array,
15 | * even if the array also contains {@code Double.POSITIVE_INFINITY}.
16 | *
17 | * @param data array to scan
18 | * @return descending list of values included in the input array
19 | * @throws NullPointerException if data is null
20 | * @since 3.6
21 | */
22 | public static int[] unique(int[] data) {
23 | TreeSet values = new TreeSet();
24 | for (int i = 0; i < data.length; i++) {
25 | values.add(data[i]);
26 | }
27 | final int count = values.size();
28 | final int[] out = new int[count];
29 | Iterator iterator = values.iterator();
30 | int i = 0;
31 | while (iterator.hasNext()) {
32 | out[count - ++i] = iterator.next();
33 | }
34 | return out;
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/PassingGrade.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | public class PassingGrade {
4 |
5 | public boolean passed(float grade) {
6 | if (grade < 1 || grade > 10)
7 | throw new IllegalArgumentException();
8 | return grade >= 5;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/Product.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import java.math.BigDecimal;
4 | import java.util.Objects;
5 |
6 | public class Product {
7 |
8 | private final String name;
9 | private BigDecimal price;
10 |
11 | public Product(String name, BigDecimal price) {
12 | this.name = name;
13 | this.price = price;
14 | }
15 |
16 | public BigDecimal getPrice() {
17 | return price;
18 | }
19 |
20 | @Override
21 | public String toString() {
22 | return "Product{" +
23 | "name='" + name + '\'' +
24 | ", price=" + price +
25 | '}';
26 | }
27 |
28 | @Override
29 | public boolean equals(Object o) {
30 | if (this == o) return true;
31 | if (o == null || getClass() != o.getClass()) return false;
32 | Product product = (Product) o;
33 | return name.equals(product.name) && price.equals(product.price);
34 | }
35 |
36 | @Override
37 | public int hashCode() {
38 | return Objects.hash(name, price);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ch5/src/main/java/ch5/Triangle.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | public class Triangle {
4 | public static boolean isTriangle(int a, int b, int c) {
5 | boolean hasABadSide = a >= (b + c) || c >= (b + a) || b >= (a + c);
6 | return !hasABadSide;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/ArrayUtilsTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import net.jqwik.api.ForAll;
4 | import net.jqwik.api.Property;
5 | import net.jqwik.api.constraints.IntRange;
6 | import net.jqwik.api.constraints.Size;
7 | import org.junit.jupiter.params.ParameterizedTest;
8 | import org.junit.jupiter.params.provider.Arguments;
9 | import org.junit.jupiter.params.provider.MethodSource;
10 |
11 | import java.util.List;
12 | import java.util.stream.Stream;
13 |
14 | import static org.assertj.core.api.Assertions.assertThat;
15 | import static org.junit.jupiter.params.provider.Arguments.of;
16 |
17 |
18 | public class ArrayUtilsTest {
19 |
20 | @ParameterizedTest
21 | @MethodSource("testCases")
22 | void testIndexOf(int[] array, int valueToFind, int startIndex, int expectedResult) {
23 | int result = ArrayUtils.indexOf(array, valueToFind, startIndex);
24 | assertThat(result).isEqualTo(expectedResult);
25 | }
26 |
27 | static Stream testCases() {
28 | int[] array = new int[] { 1, 2, 3, 4, 5, 4, 6, 7 };
29 |
30 | return Stream.of(
31 | of(null, 1, 1, -1),
32 | of(new int[] { 1 }, 1, 0, 0),
33 | of(new int[] { 1 }, 2, 0, -1),
34 | of(array, 1, 10, -1),
35 | of(array, 2, -1, 1),
36 | of(array, 4, 6, -1),
37 | of(array, 4, 1, 3),
38 | of(array, 4, 3, 3),
39 | of(array, 4, 1, 3),
40 | of(array, 4, 4, 5),
41 | of(array, 8, 0, -1)
42 | );
43 | }
44 |
45 | @Property
46 | void indexOfShouldFindFirstValue(
47 | /*
48 | * we generate a list with 100 numbers, ranging from -1000, 1000. Range chosen
49 | * randomly.
50 | */
51 | @ForAll @Size(value = 100) List<@IntRange(min = -1000, max = 1000) Integer> numbers,
52 | /**
53 | * we generate a random number that we'll insert in the list. This number is
54 | * outside the range of the list, so that we can easily find it.
55 | */
56 | @ForAll @IntRange(min = 1001, max = 2000) int value,
57 | /** We randomly pick a place to put the element in the list */
58 | @ForAll @IntRange(max = 99) int indexToAddElement,
59 | /** We randomly pick a number to start the search in the array */
60 | @ForAll @IntRange(max = 99) int startIndex) {
61 | /* we add the number to the list in the randomly chosen position */
62 | numbers.add(indexToAddElement, value);
63 |
64 | /* we convert the list to an array, as expected by the method */
65 | int[] array = convertListToArray(numbers);
66 | /**
67 | *
68 | * if we added the element after the start index, then, we expect the method to
69 | * return the position where we inserted the element. Else, we expect the method
70 | * to return -1.
71 | */
72 | int expectedIndex = indexToAddElement >= startIndex ? indexToAddElement : -1;
73 | assertThat(ArrayUtils.indexOf(array, value, startIndex)).isEqualTo(expectedIndex);
74 | }
75 |
76 | /** Use this method to convert a list of integers to an array */
77 | private int[] convertListToArray(List numbers) {
78 | int[] array = numbers.stream().mapToInt(x -> x).toArray();
79 | return array;
80 | }
81 |
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/BasketPBTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import net.jqwik.api.*;
4 | import net.jqwik.api.stateful.Action;
5 | import net.jqwik.api.stateful.ActionSequence;
6 |
7 | import java.math.BigDecimal;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 | import java.util.Random;
11 | import java.util.Set;
12 | import java.util.stream.Collectors;
13 |
14 | import static java.math.BigDecimal.valueOf;
15 | import static org.assertj.core.api.Assertions.assertThat;
16 |
17 | public class BasketPBTest {
18 |
19 | class AddAction implements Action {
20 |
21 | private final Product product;
22 | private final int qty;
23 |
24 | public AddAction(Product product, int qty) {
25 | this.product = product;
26 | this.qty = qty;
27 | }
28 | @Override
29 | public Basket run(Basket basket) {
30 | BigDecimal currentValue = basket.getTotalValue();
31 | basket.add(product, qty);
32 |
33 | BigDecimal newProductValue = product.getPrice().multiply(valueOf(qty));
34 | BigDecimal newValue = currentValue.add(newProductValue);
35 | assertThat(basket.getTotalValue()).isEqualByComparingTo(newValue);
36 | return basket;
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return "AddAction{" +
42 | "product=" + product +
43 | ", qty=" + qty +
44 | '}';
45 | }
46 | }
47 |
48 | class RemoveAction implements Action {
49 |
50 | @Override
51 | public Basket run(Basket basket) {
52 | BigDecimal currentValue = basket.getTotalValue();
53 |
54 | Set productsInBasket = basket.products();
55 |
56 | // if the basket is empty, simply skip this action
57 | if(productsInBasket.isEmpty())
58 | return basket;
59 |
60 | // pick a random element in the basket to be removed
61 | Product randomProduct = pickRandom(productsInBasket);
62 | double currentProductQty = basket.quantityOf(randomProduct);
63 |
64 | basket.remove(randomProduct);
65 |
66 | BigDecimal basketValueWithoutRandomProduct = currentValue
67 | .subtract(randomProduct.getPrice().multiply(valueOf(currentProductQty)));
68 |
69 | assertThat(basket.getTotalValue())
70 | .isEqualByComparingTo(basketValueWithoutRandomProduct);
71 |
72 | return basket;
73 | }
74 |
75 | private Product pickRandom(Set set){
76 |
77 | Random random = new Random();
78 | int randomNumber = random.nextInt(set.size());
79 |
80 | int currentIndex = 0;
81 | Product randomElement = null;
82 |
83 | for(Product element : set){
84 | randomElement = element;
85 |
86 | if(currentIndex == randomNumber)
87 | return randomElement;
88 |
89 | currentIndex++;
90 | }
91 |
92 | return randomElement;
93 | }
94 |
95 | @Override
96 | public String toString() {
97 | return "RemoveAction";
98 | }
99 | }
100 |
101 | static List randomProducts = new ArrayList<>() {{
102 | add(new Product("TV", new BigDecimal("100")));
103 | add(new Product("Playstation", new BigDecimal("150.3")));
104 | add(new Product("Refrigerator", new BigDecimal("180.27")));
105 | add(new Product("Soda", new BigDecimal("2.69")));
106 | }};
107 |
108 | private Arbitrary addAction() {
109 | // create an arbitrary product out of the list of pre-defined products
110 | Arbitrary products = Arbitraries.oneOf(
111 | randomProducts
112 | .stream()
113 | .map(product -> Arbitraries.of(product))
114 | .collect(Collectors.toList()));
115 |
116 | // create arbitrary quantities
117 | Arbitrary qtys = Arbitraries.integers().between(1, 100);
118 |
119 | // now, we combine products and qtys and generate 'add actions'
120 | return Combinators
121 | .combine(products, qtys)
122 | .as((product, qty) -> new AddAction(product, qty));
123 | }
124 |
125 | @Property(afterFailure = AfterFailureMode.SAMPLE_ONLY)
126 | void sequenceOfAddsAndRemoves(@ForAll("addsAndRemoves") ActionSequence actions) {
127 | actions.run(new Basket());
128 | }
129 |
130 | @Provide
131 | Arbitrary> addsAndRemoves() {
132 | // generate arbitrary sequences of adds and removes
133 | return Arbitraries.sequences(Arbitraries.oneOf(
134 | addAction(),
135 | removeAction()));
136 | }
137 |
138 | private Arbitrary removeAction() {
139 | return Arbitraries.of(new RemoveAction());
140 | }
141 |
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/BasketTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static java.math.BigDecimal.valueOf;
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class BasketTest {
9 | private Basket basket = new Basket();
10 |
11 | @Test
12 | void addProducts() {
13 |
14 | basket.add(new Product("TV", valueOf(10)), 2);
15 | basket.add(new Product("Playstation", valueOf(100)), 1);
16 |
17 | assertThat(basket.getTotalValue())
18 | .isEqualByComparingTo(valueOf(10*2 + 100*1));
19 | }
20 |
21 | @Test
22 | void addSameProductTwice() {
23 |
24 | Product p = new Product("TV", valueOf(10));
25 | basket.add(p, 2);
26 | basket.add(p, 3);
27 |
28 | assertThat(basket.getTotalValue())
29 | .isEqualTo(valueOf(10*5));
30 | }
31 |
32 | @Test
33 | void removeProducts() {
34 |
35 | basket.add(new Product("TV", valueOf(100)), 1);
36 |
37 | Product p = new Product("Playstation", valueOf(10));
38 | basket.add(p, 2);
39 | basket.remove(p);
40 |
41 | assertThat(basket.getTotalValue())
42 | .isEqualTo(valueOf(100));
43 | }
44 |
45 | // tests for exceptional cases...
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/BookTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import net.jqwik.api.*;
4 |
5 | public class BookTest {
6 |
7 | @Property
8 | void differentBooks(@ForAll("books") Book book) {
9 | // different books!
10 | System.out.println(book);
11 |
12 | // write your test here!
13 |
14 | }
15 |
16 | @Provide
17 | Arbitrary books() {
18 | Arbitrary titles = Arbitraries.strings().withCharRange('a', 'z')
19 | .ofMinLength(10).ofMaxLength(100);
20 | Arbitrary authors = Arbitraries.strings().withCharRange('a', 'z')
21 | .ofMinLength(5).ofMaxLength(21);
22 | Arbitrary qtyOfPages = Arbitraries.integers().between(0, 450);
23 | return Combinators.combine(titles, authors, qtyOfPages)
24 | .as((title, author, pages) -> new Book(title, author, pages));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/MathArraysPBTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import net.jqwik.api.ForAll;
4 | import net.jqwik.api.Property;
5 | import net.jqwik.api.constraints.IntRange;
6 | import net.jqwik.api.constraints.Size;
7 |
8 | import java.util.Comparator;
9 | import java.util.List;
10 |
11 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
12 |
13 | public class MathArraysPBTest {
14 |
15 | @Property
16 | // an array of size 100 with doubles between [0,20] will definitely have repeated numbers
17 | void unique(@ForAll @Size(value = 100) List<@IntRange(min = 1, max = 20) Integer> numbers) {
18 |
19 | int[] doubles = convertListToArray(numbers);
20 | int[] result = MathArrays.unique(doubles);
21 |
22 | assertThat(result)
23 | .contains(doubles) // contains all the elements
24 | .doesNotHaveDuplicates() // no duplicates
25 | .isSortedAccordingTo(Comparator.reverseOrder()); // in descending order
26 | }
27 |
28 | /** Use this method to convert a list of integers to an array */
29 | private int[] convertListToArray(List numbers) {
30 | int[] array = numbers.stream().mapToInt(x -> x).toArray();
31 | return array;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/PassingGradePBTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import net.jqwik.api.*;
4 | import net.jqwik.api.constraints.FloatRange;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 | import static org.assertj.core.api.Assertions.assertThatThrownBy;
8 |
9 | public class PassingGradePBTest {
10 |
11 | private final PassingGrade pg = new PassingGrade();
12 |
13 | @Property
14 | void fail(@ForAll @FloatRange(min = 1f, max = 5.0f, maxIncluded = false) float grade) {
15 | assertThat(pg.passed(grade)).isFalse();
16 | }
17 |
18 | @Property
19 | void pass(@ForAll @FloatRange(min = 5.0f, max = 10.0f, maxIncluded = true) float grade) {
20 | assertThat(pg.passed(grade)).isTrue();
21 | }
22 |
23 | @Property
24 | void invalid(@ForAll("invalidGrades") float grade) {
25 | assertThatThrownBy(() -> {
26 | pg.passed(grade);
27 | })
28 | .isInstanceOf(IllegalArgumentException.class);
29 | }
30 |
31 | @Provide
32 | private Arbitrary invalidGrades() {
33 | return Arbitraries.oneOf(
34 | Arbitraries.floats().lessThan(1f),
35 | Arbitraries.floats().greaterThan(10f));
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/ch5/src/test/java/ch5/TriangleTest.java:
--------------------------------------------------------------------------------
1 | package ch5;
2 |
3 | import net.jqwik.api.*;
4 | import net.jqwik.api.constraints.IntRange;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class TriangleTest {
9 |
10 | @Property
11 | void triangleBadTest(@ForAll @IntRange(max = 100) int a,
12 | @ForAll @IntRange(max = 100) int b,
13 | @ForAll @IntRange(max = 100) int c) {
14 | // ... test here ...
15 |
16 | }
17 |
18 |
19 | @Property
20 | void triangleIsInvalidIfOneSideIsBiggerThanOthers(@ForAll("invalidTrianglesGenerator") ABC abc) {
21 | assertThat(Triangle.isTriangle(abc.a, abc.b, abc.c)).isFalse();
22 | }
23 |
24 | @Provide
25 | Arbitrary invalidTrianglesGenerator() {
26 | Arbitrary normalSide1 = Arbitraries.integers();
27 | Arbitrary normalSide2 = Arbitraries.integers();
28 | Arbitrary biggerSide = Arbitraries.integers();
29 | Arbitrary biggerA = Combinators.combine(normalSide1, normalSide2, biggerSide).as(ABC::new)
30 | .filter(k -> k.a >= k.b + k.c);
31 | Arbitrary biggerB = Combinators.combine(normalSide1, normalSide2, biggerSide).as(ABC::new)
32 | .filter(k -> k.b >= k.a + k.c);
33 | Arbitrary biggerC = Combinators.combine(normalSide1, normalSide2, biggerSide).as(ABC::new)
34 | .filter(k -> k.c >= k.a + k.b);
35 | return Arbitraries.oneOf(biggerA, biggerB, biggerC);
36 | }
37 |
38 | @Property
39 | void triangleIsValidOtherwise(@ForAll("validTriangleGenerator") ABC abc) {
40 | assertThat(Triangle.isTriangle(abc.a, abc.b, abc.c)).isTrue();
41 | }
42 |
43 | @Provide
44 | Arbitrary validTriangleGenerator() {
45 | Arbitrary normalSide1 = Arbitraries.integers();
46 | Arbitrary normalSide2 = Arbitraries.integers();
47 | Arbitrary normalSide3 = Arbitraries.integers();
48 | return Combinators.combine(normalSide1, normalSide2, normalSide3).as(ABC::new)
49 | .filter(k -> (k.a < k.b + k.c) && (k.b < k.a + k.c) && (k.c < k.a + k.b));
50 | }
51 |
52 | // use the ABC class below.
53 | class ABC {
54 |
55 | int a;
56 |
57 | int b;
58 |
59 | int c;
60 |
61 | public ABC(int a, int b, int c) {
62 | this.a = a;
63 | this.b = b;
64 | this.c = c;
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/ch6/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch6
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.hsqldb
20 | hsqldb
21 | 2.6.0
22 | test
23 |
24 |
25 |
26 |
27 | org.assertj
28 | assertj-core
29 | 3.15.0
30 | test
31 |
32 |
33 |
34 |
35 | org.junit.jupiter
36 | junit-jupiter-engine
37 | 5.6.2
38 | test
39 |
40 |
41 |
42 |
43 | org.junit.jupiter
44 | junit-jupiter-params
45 | 5.6.2
46 | test
47 |
48 |
49 |
50 |
51 | org.mockito
52 | mockito-core
53 | 3.11.2
54 | test
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | maven-surefire-plugin
65 | 3.0.0-M5
66 |
67 | true
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/arguments/InvoiceToSapInvoiceConverter.java:
--------------------------------------------------------------------------------
1 | package ch6.arguments;
2 |
3 | import ch6.stub.Invoice;
4 |
5 | import java.time.LocalDate;
6 | import java.time.format.DateTimeFormatter;
7 |
8 | public class InvoiceToSapInvoiceConverter {
9 |
10 | public SapInvoice convert(Invoice invoice) {
11 | String customer = invoice.getCustomer();
12 | int value = invoice.getValue();
13 | String sapId = generateId(invoice);
14 |
15 | SapInvoice sapInvoice = new SapInvoice(customer, value, sapId);
16 | return sapInvoice;
17 | }
18 |
19 | private String generateId(Invoice invoice) {
20 | String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
21 | String customer = invoice.getCustomer();
22 | return date + (customer.length()>=2 ? customer.substring(0,2) : "X");
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/arguments/SAP.java:
--------------------------------------------------------------------------------
1 | package ch6.arguments;
2 |
3 | public interface SAP {
4 | void send(SapInvoice invoice);
5 | }
6 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/arguments/SAPInvoiceSender.java:
--------------------------------------------------------------------------------
1 | package ch6.arguments;
2 |
3 | import ch6.stub.Invoice;
4 | import ch6.stub.InvoiceFilter;
5 |
6 | import java.time.LocalDate;
7 | import java.time.format.DateTimeFormatter;
8 | import java.util.List;
9 |
10 | public class SAPInvoiceSender {
11 |
12 | private final InvoiceFilter filter;
13 | private final SAP sap;
14 |
15 | public SAPInvoiceSender(InvoiceFilter filter, SAP sap) {
16 | this.filter = filter;
17 | this.sap = sap;
18 | }
19 |
20 | public void sendLowValuedInvoices() {
21 | List lowValuedInvoices = filter.lowValueInvoices();
22 | for(Invoice invoice : lowValuedInvoices) {
23 | String customer = invoice.getCustomer();
24 | int value = invoice.getValue();
25 | String sapId = generateId(invoice);
26 |
27 | SapInvoice sapInvoice = new SapInvoice(customer, value, sapId);
28 | sap.send(sapInvoice);
29 | }
30 | }
31 |
32 | private String generateId(Invoice invoice) {
33 | String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
34 | String customer = invoice.getCustomer();
35 | return date + (customer.length()>=2 ? customer.substring(0,2) : "X");
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/arguments/SapInvoice.java:
--------------------------------------------------------------------------------
1 | package ch6.arguments;
2 |
3 | import java.util.Objects;
4 |
5 | public class SapInvoice {
6 | private final String customer;
7 | private final int value;
8 | private final String id;
9 |
10 | public SapInvoice(String customer, int value, String id) {
11 | assert customer!=null;
12 | assert id!=null;
13 |
14 | this.customer = customer;
15 | this.value = value;
16 | this.id = id;
17 | }
18 |
19 | public String getCustomer() {
20 | return customer;
21 | }
22 |
23 | public int getValue() {
24 | return value;
25 | }
26 |
27 | public String getId() {
28 | return id;
29 | }
30 |
31 | @Override
32 | public boolean equals(Object o) {
33 | if (this == o) return true;
34 | if (o == null || getClass() != o.getClass()) return false;
35 | SapInvoice that = (SapInvoice) o;
36 | return value == that.value && customer.equals(that.customer) && id.equals(that.id);
37 | }
38 |
39 | @Override
40 | public int hashCode() {
41 | return Objects.hash(customer, value, id);
42 | }
43 |
44 | @Override
45 | public String toString() {
46 | return "SapInvoice{" +
47 | "customer='" + customer + '\'' +
48 | ", value=" + value +
49 | ", id='" + id + '\'' +
50 | '}';
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/bookstore/Book.java:
--------------------------------------------------------------------------------
1 | package ch6.bookstore;
2 |
3 | import java.util.Objects;
4 |
5 | public class Book {
6 | private String ISBN;
7 | private int price;
8 | private int amount;
9 |
10 | public Book(String ISBN, int price, int amount) {
11 | this.ISBN = ISBN;
12 | this.price = price;
13 | this.amount = amount;
14 | }
15 |
16 | public int getPrice() {
17 | return price;
18 | }
19 | public int getAmount() {
20 | return amount;
21 | }
22 |
23 | @Override
24 | public boolean equals(Object o) {
25 | if (this == o) return true;
26 | if (o == null || getClass() != o.getClass()) return false;
27 | Book book = (Book) o;
28 | return ISBN.equals(book.ISBN);
29 | }
30 |
31 | @Override
32 | public int hashCode() {
33 | return Objects.hash(ISBN);
34 | }
35 | }
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/bookstore/BookRepository.java:
--------------------------------------------------------------------------------
1 | package ch6.bookstore;
2 |
3 | public interface BookRepository {
4 | Book findByISBN(String ISBN);
5 | }
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/bookstore/BookStore.java:
--------------------------------------------------------------------------------
1 | package ch6.bookstore;
2 |
3 | import java.util.Map;
4 |
5 | class BookStore {
6 |
7 | private BookRepository bookRepository;
8 | private BuyBookProcess process;
9 |
10 | public BookStore(BookRepository bookRepository, BuyBookProcess process) {
11 | this.bookRepository = bookRepository;
12 | this.process = process;
13 | }
14 |
15 | private void retrieveBook(String ISBN, int amount, Overview overview) {
16 | Book book = bookRepository.findByISBN(ISBN);
17 | if (book.getAmount() < amount) {
18 | overview.addUnavailable(book, amount - book.getAmount());
19 | amount = book.getAmount();
20 | }
21 |
22 | overview.addToTotalPrice(amount * book.getPrice());
23 | process.buyBook(book, amount);
24 | }
25 |
26 | public Overview getPriceForCart(Map order) {
27 | if(order==null)
28 | return null;
29 |
30 | Overview overview = new Overview();
31 | for (String ISBN : order.keySet())
32 | retrieveBook(ISBN, order.get(ISBN), overview);
33 | return overview;
34 | }
35 | }
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/bookstore/BuyBookProcess.java:
--------------------------------------------------------------------------------
1 | package ch6.bookstore;
2 |
3 | public interface BuyBookProcess {
4 | void buyBook(Book book, int amount);
5 | }
6 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/bookstore/Overview.java:
--------------------------------------------------------------------------------
1 | package ch6.bookstore;
2 |
3 | import java.util.Collections;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 |
7 | public class Overview {
8 | private int totalPrice;
9 | private Map unavailable;
10 |
11 | public Overview() {
12 | this.unavailable = new HashMap<>();
13 | this.totalPrice = 0;
14 | }
15 |
16 | public void addUnavailable(Book book, int unavailableQty){
17 | this.unavailable.put(book, unavailableQty);
18 | }
19 |
20 | public void addToTotalPrice(int valueToAdd) {
21 | totalPrice += valueToAdd;
22 | }
23 |
24 | public int getTotalPrice() {
25 | return totalPrice;
26 | }
27 |
28 | public Map getUnavailable() {
29 | return Collections.unmodifiableMap(unavailable);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/christmas/ChristmasDiscount.java:
--------------------------------------------------------------------------------
1 | package ch6.christmas;
2 |
3 | import java.time.LocalDate;
4 | import java.time.Month;
5 |
6 | public class ChristmasDiscount {
7 |
8 | private final Clock clock;
9 |
10 | public ChristmasDiscount(Clock clock) {
11 | this.clock = clock;
12 | }
13 |
14 | public double applyDiscount(double rawAmount) {
15 | LocalDate today = clock.now();
16 |
17 | double discountPercentage = 0;
18 | boolean isChristmas = today.getMonth()== Month.DECEMBER
19 | && today.getDayOfMonth()==25;
20 |
21 | if(isChristmas)
22 | discountPercentage = 0.15;
23 |
24 | return rawAmount - (rawAmount * discountPercentage);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/christmas/Clock.java:
--------------------------------------------------------------------------------
1 | package ch6.christmas;
2 |
3 | import java.time.LocalDate;
4 |
5 | public class Clock {
6 | public LocalDate now() {
7 | return LocalDate.now();
8 | }
9 |
10 | // any other date and time operation here...
11 | }
12 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/exception/SAP.java:
--------------------------------------------------------------------------------
1 | package ch6.exception;
2 |
3 | public interface SAP {
4 | void send(SapInvoice invoice);
5 | }
6 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/exception/SAPException.java:
--------------------------------------------------------------------------------
1 | package ch6.exception;
2 |
3 | public class SAPException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/exception/SAPInvoiceSender.java:
--------------------------------------------------------------------------------
1 | package ch6.exception;
2 |
3 | import ch6.stub.Invoice;
4 | import ch6.stub.InvoiceFilter;
5 |
6 | import java.time.LocalDate;
7 | import java.time.format.DateTimeFormatter;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public class SAPInvoiceSender {
12 |
13 | private final InvoiceFilter filter;
14 | private final SAP sap;
15 |
16 | public SAPInvoiceSender(InvoiceFilter filter, SAP sap) {
17 | this.filter = filter;
18 | this.sap = sap;
19 | }
20 |
21 | public List sendLowValuedInvoices() {
22 | List failedInvoices = new ArrayList<>();
23 |
24 | List lowValuedInvoices = filter.lowValueInvoices();
25 | for(Invoice invoice : lowValuedInvoices) {
26 | String customer = invoice.getCustomer();
27 | int value = invoice.getValue();
28 | String sapId = generateId(invoice);
29 |
30 | SapInvoice sapInvoice = new SapInvoice(customer, value, sapId);
31 | try {
32 | sap.send(sapInvoice);
33 | } catch(SAPException e) {
34 | failedInvoices.add(invoice);
35 | }
36 | }
37 |
38 | return failedInvoices;
39 | }
40 |
41 | private String generateId(Invoice invoice) {
42 | String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
43 | String customer = invoice.getCustomer();
44 | return date + (customer.length()>=2 ? customer.substring(0,2) : "X");
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/exception/SapInvoice.java:
--------------------------------------------------------------------------------
1 | package ch6.exception;
2 |
3 | import java.util.Objects;
4 |
5 | public class SapInvoice {
6 | private final String customer;
7 | private final int value;
8 | private final String id;
9 |
10 | public SapInvoice(String customer, int value, String id) {
11 | assert customer!=null;
12 | assert id!=null;
13 |
14 | this.customer = customer;
15 | this.value = value;
16 | this.id = id;
17 | }
18 |
19 | public String getCustomer() {
20 | return customer;
21 | }
22 |
23 | public int getValue() {
24 | return value;
25 | }
26 |
27 | public String getId() {
28 | return id;
29 | }
30 |
31 | @Override
32 | public boolean equals(Object o) {
33 | if (this == o) return true;
34 | if (o == null || getClass() != o.getClass()) return false;
35 | SapInvoice that = (SapInvoice) o;
36 | return value == that.value && customer.equals(that.customer) && id.equals(that.id);
37 | }
38 |
39 | @Override
40 | public int hashCode() {
41 | return Objects.hash(customer, value, id);
42 | }
43 |
44 | @Override
45 | public String toString() {
46 | return "SapInvoice{" +
47 | "customer='" + customer + '\'' +
48 | ", value=" + value +
49 | ", id='" + id + '\'' +
50 | '}';
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/mock/SAP.java:
--------------------------------------------------------------------------------
1 | package ch6.mock;
2 |
3 | import ch6.stub.Invoice;
4 |
5 | public interface SAP {
6 | void send(Invoice invoice);
7 | }
8 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/mock/SAPInvoiceSender.java:
--------------------------------------------------------------------------------
1 | package ch6.mock;
2 |
3 | import ch6.stub.Invoice;
4 | import ch6.stub.InvoiceFilter;
5 |
6 | import java.util.List;
7 |
8 | public class SAPInvoiceSender {
9 |
10 | private final InvoiceFilter filter;
11 | private final SAP sap;
12 |
13 | public SAPInvoiceSender(InvoiceFilter filter, SAP sap) {
14 | this.filter = filter;
15 | this.sap = sap;
16 | }
17 |
18 | public void sendLowValuedInvoices() {
19 | List lowValuedInvoices = filter.lowValueInvoices();
20 | for(Invoice invoice : lowValuedInvoices) {
21 | sap.send(invoice);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/stub/DatabaseConnection.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import java.sql.Connection;
4 | import java.sql.DriverManager;
5 | import java.sql.SQLException;
6 |
7 | /**
8 | * This is a very naive database connection class.
9 | * In real life, you should make use of a decent database API,
10 | * such as Spring Data or Hibernate.
11 | */
12 | public class DatabaseConnection {
13 |
14 | private static Connection connection;
15 |
16 | public DatabaseConnection() {
17 | if(connection !=null) return;
18 |
19 | withSql(() -> {
20 | connection = DriverManager.getConnection("jdbc:hsqldb:mem:mymemdb.db", "SA", "");
21 | try (var preparedStatement = connection.prepareStatement("create table if not exists invoice (name varchar(100), value double)")) {
22 | preparedStatement.execute();
23 | connection.commit();
24 | }
25 | return null;
26 | });
27 | }
28 |
29 | public Connection getConnection() {
30 | return connection;
31 | }
32 |
33 | public void resetDatabase() {
34 | withSql(() -> {
35 | connection = DriverManager.getConnection("jdbc:hsqldb:mem:mymemdb.db", "SA", "");
36 | try (var preparedStatement = connection.prepareStatement("delete from invoice")) {
37 | preparedStatement.execute();
38 | connection.commit();
39 | }
40 | return null;
41 | });
42 | }
43 |
44 | public interface SqlSupplier {
45 | T doSql() throws SQLException;
46 | }
47 | public T withSql(SqlSupplier sqlSupplier) {
48 | try {
49 | return sqlSupplier.doSql();
50 | } catch (SQLException e) {
51 | throw new RuntimeException(e);
52 | }
53 | }
54 |
55 | public void close() {
56 | withSql( () -> {
57 | if (connection != null) {
58 | connection.close();
59 | }
60 | return null;
61 | });
62 | connection = null;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/stub/Invoice.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import java.util.Objects;
4 |
5 | public class Invoice {
6 | private final String customer;
7 | private final int value;
8 |
9 | public Invoice(String customer, int value) {
10 | this.customer = customer;
11 | this.value = value;
12 | }
13 |
14 |
15 |
16 | @Override
17 | public boolean equals(Object o) {
18 | if (this == o) return true;
19 | if (o == null || getClass() != o.getClass()) return false;
20 | Invoice invoice = (Invoice) o;
21 | return value == invoice.value &&
22 | customer.equals(invoice.customer);
23 | }
24 |
25 | @Override
26 | public int hashCode() {
27 | return Objects.hash(customer, value);
28 | }
29 |
30 | @Override
31 | public String toString() {
32 | return "Invoice{" +
33 | "customer='" + customer + '\'' +
34 | ", value=" + value +
35 | '}';
36 | }
37 |
38 | public String getCustomer() {
39 | return customer;
40 | }
41 |
42 | public int getValue() {
43 | return value;
44 | }
45 | }
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/stub/InvoiceFilter.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import java.util.List;
4 |
5 | import static java.util.stream.Collectors.toList;
6 |
7 | public class InvoiceFilter {
8 |
9 | private final IssuedInvoices issuedInvoices;
10 |
11 | public InvoiceFilter(IssuedInvoices issuedInvoices) {
12 | this.issuedInvoices = issuedInvoices;
13 | }
14 | public List lowValueInvoices() {
15 | List all = issuedInvoices.all();
16 |
17 | return all.stream()
18 | .filter(invoice -> invoice.getValue() < 100)
19 | .collect(toList());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/stub/InvoiceFilterWithDatabase.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import java.util.List;
4 |
5 | import static java.util.stream.Collectors.toList;
6 |
7 | public class InvoiceFilterWithDatabase {
8 |
9 | public List lowValueInvoices() {
10 | DatabaseConnection connection = new DatabaseConnection();
11 | IssuedInvoices issuedInvoices = new IssuedInvoices(connection);
12 |
13 | try {
14 | List all = issuedInvoices.all();
15 |
16 | return all.stream()
17 | .filter(invoice -> invoice.getValue() < 100)
18 | .collect(toList());
19 | } finally {
20 | connection.close();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ch6/src/main/java/ch6/stub/IssuedInvoices.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | public class IssuedInvoices {
7 |
8 | private DatabaseConnection connection;
9 |
10 | public IssuedInvoices(DatabaseConnection connection) {
11 | this.connection = connection;
12 | }
13 |
14 | public List all() {
15 | return connection.withSql( () -> {
16 | try (var ps = connection.getConnection().prepareStatement("select * from invoice")) {
17 | final var rs = ps.executeQuery();
18 |
19 | List allInvoices = new ArrayList<>();
20 | while (rs.next()) {
21 | allInvoices.add(new Invoice(rs.getString("name"), rs.getInt("value")));
22 | }
23 | return allInvoices;
24 | }
25 | });
26 | }
27 |
28 | public List allWithAtLeast(int value) {
29 | return connection.withSql( () -> {
30 | try (var ps = connection.getConnection().prepareStatement("select * from invoice where value >= ?")) {
31 | ps.setInt(1, value);
32 | final var rs = ps.executeQuery();
33 |
34 | List allInvoices = new ArrayList<>();
35 | while (rs.next()) {
36 | allInvoices.add(new Invoice(rs.getString("name"), rs.getInt("value")));
37 | }
38 | return allInvoices;
39 | }
40 | });
41 | }
42 |
43 | public void save(Invoice inv) {
44 | connection.withSql( () -> {
45 | try (var ps = connection.getConnection().prepareStatement("insert into invoice (name, value) values (?,?)")) {
46 | ps.setString(1, inv.getCustomer());
47 | ps.setInt(2, inv.getValue());
48 | ps.execute();
49 |
50 | connection.getConnection().commit();
51 | }
52 | return null;
53 | });
54 | }
55 |
56 |
57 |
58 | }
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/arguments/SAPInvoiceSenderTest.java:
--------------------------------------------------------------------------------
1 | package ch6.arguments;
2 |
3 | import ch6.stub.Invoice;
4 | import ch6.stub.InvoiceFilter;
5 | import org.junit.jupiter.api.Test;
6 | import org.junit.jupiter.params.ParameterizedTest;
7 | import org.junit.jupiter.params.provider.CsvSource;
8 | import org.mockito.ArgumentCaptor;
9 |
10 | import java.time.LocalDate;
11 | import java.time.format.DateTimeFormatter;
12 | import java.util.Arrays;
13 | import java.util.List;
14 |
15 | import static org.assertj.core.api.Assertions.assertThat;
16 | import static org.mockito.Mockito.*;
17 |
18 | public class SAPInvoiceSenderTest {
19 |
20 | private InvoiceFilter filter = mock(InvoiceFilter.class);
21 | private SAP sap = mock(SAP.class);
22 | private SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);
23 |
24 | @ParameterizedTest
25 | @CsvSource({
26 | "Mauricio,Ma",
27 | "M,X"}
28 | )
29 | void sendToSapWithTheGeneratedId(String customer, String initialId) {
30 | Invoice mauricio = new Invoice(customer, 20);
31 |
32 | List invoices = Arrays.asList(mauricio);
33 | when(filter.lowValueInvoices()).thenReturn(invoices);
34 |
35 | sender.sendLowValuedInvoices();
36 |
37 | ArgumentCaptor captor = ArgumentCaptor.forClass(SapInvoice.class);
38 | verify(sap).send(captor.capture());
39 |
40 | SapInvoice generatedSapInvoice = captor.getValue();
41 |
42 | String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
43 | assertThat(generatedSapInvoice).isEqualTo(new SapInvoice(customer, 20, date + initialId));
44 | }
45 |
46 |
47 | @Test
48 | void oldExample() {
49 | Invoice mauricio = new Invoice("Mauricio", 20);
50 |
51 | List invoices = Arrays.asList(mauricio);
52 | when(filter.lowValueInvoices()).thenReturn(invoices);
53 |
54 | sender.sendLowValuedInvoices();
55 |
56 | verify(sap).send(any(SapInvoice.class));
57 | }
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/bookstore/BookStoreTest.java:
--------------------------------------------------------------------------------
1 | package ch6.bookstore;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.HashMap;
6 | import java.util.Map;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.assertj.core.api.Assertions.entry;
10 | import static org.mockito.Mockito.*;
11 |
12 | public class BookStoreTest {
13 |
14 | @Test
15 | void emptyOrder() {
16 | BookRepository bookRepo = mock(BookRepository.class);
17 | BuyBookProcess process = mock(BuyBookProcess.class);
18 | BookStore bookStore = new BookStore(bookRepo, process);
19 |
20 | Map orderMap = new HashMap<>();
21 | Overview overview = bookStore.getPriceForCart(orderMap);
22 |
23 | assertThat(overview.getTotalPrice()).isEqualTo(0);
24 | assertThat(overview.getUnavailable()).isEmpty();
25 | }
26 |
27 | @Test
28 | void nullOrder() {
29 | BookRepository bookRepo = mock(BookRepository.class);
30 | BuyBookProcess process = mock(BuyBookProcess.class);
31 | BookStore bookStore = new BookStore(bookRepo, process);
32 |
33 | Overview overview = bookStore.getPriceForCart(null);
34 |
35 | assertThat(overview).isNull();
36 | }
37 |
38 | @Test
39 | void moreComplexOrder() {
40 | BookRepository bookRepo = mock(BookRepository.class);
41 | BuyBookProcess process = mock(BuyBookProcess.class);
42 |
43 | Map orderMap = new HashMap<>();
44 |
45 | /**
46 | * Let's have three books:
47 | * - one where there's enough quantity
48 | * - one where the available quantity is precisely what's asked in the order
49 | * - one where there's not enough quantity
50 | */
51 | orderMap.put("PRODUCT-ENOUGH-QTY", 5);
52 | orderMap.put("PRODUCT-PRECISE-QTY", 10);
53 | orderMap.put("PRODUCT-NOT-ENOUGH", 22);
54 |
55 | Book book1 = new Book("PRODUCT-ENOUGH-QTY", 20, 11); // 11 is more than 5
56 | when(bookRepo.findByISBN("PRODUCT-ENOUGH-QTY")).thenReturn(book1);
57 | Book book2 = new Book("PRODUCT-PRECISE-QTY", 25, 10); // 10 == 10
58 | when(bookRepo.findByISBN("PRODUCT-PRECISE-QTY")).thenReturn(book2);
59 | Book book3 = new Book("PRODUCT-NOT-ENOUGH", 37, 21); // 21 < 22
60 | when(bookRepo.findByISBN("PRODUCT-NOT-ENOUGH")).thenReturn(book3);
61 |
62 | BookStore bookStore = new BookStore(bookRepo, process);
63 | Overview overview = bookStore.getPriceForCart(orderMap);
64 |
65 | // First, we ensure that the total price is correct
66 | int expectedPrice =
67 | 5*20 + // from the first product
68 | 10*25 + // from the second product
69 | 21*37; // from the third product
70 |
71 | assertThat(overview.getTotalPrice()).isEqualTo(expectedPrice);
72 |
73 | // Then, we ensure that the buy process was called
74 | verify(process).buyBook(book1, 5);
75 | verify(process).buyBook(book2, 10);
76 | verify(process).buyBook(book3, 21);
77 |
78 | // Finally, we ensure that the list of unavailable contains the one book that's missing
79 | assertThat(overview.getUnavailable())
80 | .containsExactly(entry(book3, 1));
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/christmas/ChristmasDiscountTest.java:
--------------------------------------------------------------------------------
1 | package ch6.christmas;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.time.LocalDate;
6 | import java.time.Month;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.assertj.core.data.Offset.offset;
10 | import static org.junit.jupiter.api.Assertions.assertEquals;
11 | import static org.mockito.Mockito.mock;
12 | import static org.mockito.Mockito.when;
13 |
14 | public class ChristmasDiscountTest {
15 |
16 | private final Clock clock = mock(Clock.class);
17 | private final ChristmasDiscount cd = new ChristmasDiscount(clock);
18 |
19 | @Test
20 | public void christmas() {
21 | LocalDate christmas = LocalDate.of(2015, Month.DECEMBER, 25);
22 | when(clock.now()).thenReturn(christmas);
23 |
24 | double finalValue = cd.applyDiscount(100.0);
25 | assertThat(finalValue).isCloseTo(85.0, offset(0.001));
26 | }
27 |
28 | @Test
29 | public void notChristmas() {
30 | LocalDate notChristmas = LocalDate.of(2015, Month.DECEMBER, 26);
31 | when(clock.now()).thenReturn(notChristmas);
32 |
33 | double finalValue = cd.applyDiscount(100.0);
34 | assertThat(finalValue).isCloseTo(100.0, offset(0.001));
35 | }
36 | }
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/exception/SAPInvoiceSenderTest.java:
--------------------------------------------------------------------------------
1 | package ch6.exception;
2 |
3 | import ch6.stub.Invoice;
4 | import ch6.stub.InvoiceFilter;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.time.LocalDate;
8 | import java.time.format.DateTimeFormatter;
9 | import java.util.Arrays;
10 | import java.util.List;
11 |
12 | import static org.assertj.core.api.Assertions.assertThat;
13 | import static org.mockito.Mockito.*;
14 |
15 | public class SAPInvoiceSenderTest {
16 |
17 | private InvoiceFilter filter = mock(InvoiceFilter.class);
18 | private SAP sap = mock(SAP.class);
19 | private SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);
20 |
21 | @Test
22 | void returnFailedInvoices() {
23 | Invoice mauricio = new Invoice("Mauricio", 20);
24 | Invoice frank = new Invoice("Frank", 25);
25 | Invoice steve = new Invoice("Steve", 48);
26 |
27 | List invoices = Arrays.asList(mauricio, frank, steve);
28 | when(filter.lowValueInvoices()).thenReturn(invoices);
29 |
30 | String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
31 | SapInvoice franksInvoice = new SapInvoice("Frank", 25, date + "Fr");
32 | doThrow(new SAPException()).when(sap).send(franksInvoice);
33 |
34 | List failedInvoices = sender.sendLowValuedInvoices();
35 | assertThat(failedInvoices).containsExactly(frank);
36 |
37 | SapInvoice mauriciosInvoice = new SapInvoice("Mauricio", 20, date + "Ma");
38 | verify(sap).send(mauriciosInvoice);
39 |
40 | SapInvoice stevesInvoice = new SapInvoice("Steve", 48, date + "St");
41 | verify(sap).send(stevesInvoice);
42 |
43 |
44 | }
45 |
46 |
47 |
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/mock/SAPInvoiceSenderTest.java:
--------------------------------------------------------------------------------
1 | package ch6.mock;
2 |
3 | import ch6.stub.Invoice;
4 | import ch6.stub.InvoiceFilter;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.util.Arrays;
8 | import java.util.List;
9 |
10 | import static java.util.Collections.emptyList;
11 | import static org.mockito.ArgumentMatchers.any;
12 | import static org.mockito.Mockito.*;
13 |
14 | public class SAPInvoiceSenderTest {
15 |
16 | private InvoiceFilter filter = mock(InvoiceFilter.class);
17 | private SAP sap = mock(SAP.class);
18 | private SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);
19 |
20 | @Test
21 | void sendToSap() {
22 | Invoice mauricio = new Invoice("Mauricio", 20);
23 | Invoice frank = new Invoice("Frank", 99);
24 |
25 | List invoices = Arrays.asList(mauricio, frank);
26 | when(filter.lowValueInvoices()).thenReturn(invoices);
27 |
28 | sender.sendLowValuedInvoices();
29 |
30 | verify(sap).send(mauricio);
31 | verify(sap).send(frank);
32 | }
33 |
34 | @Test
35 | void noLowValueInvoices() {
36 | List invoices = emptyList();
37 | when(filter.lowValueInvoices()).thenReturn(invoices);
38 |
39 | sender.sendLowValuedInvoices();
40 |
41 | verify(sap, never()).send(any(Invoice.class));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/stub/InvoiceFilterTest.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.Arrays;
6 | import java.util.List;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.mockito.Mockito.mock;
10 | import static org.mockito.Mockito.when;
11 |
12 | public class InvoiceFilterTest {
13 |
14 | @Test
15 | void filterInvoices() {
16 | IssuedInvoices issuedInvoices = mock(IssuedInvoices.class);
17 |
18 | Invoice mauricio = new Invoice("Mauricio", 20);
19 | Invoice steve = new Invoice("Steve", 99);
20 | Invoice frank = new Invoice("Frank", 100);
21 |
22 | List listOfInvoices = Arrays.asList(mauricio, steve, frank);
23 | when(issuedInvoices.all()).thenReturn(listOfInvoices);
24 |
25 | InvoiceFilter filter = new InvoiceFilter(issuedInvoices);
26 |
27 | assertThat(filter.lowValueInvoices())
28 | .containsExactlyInAnyOrder(mauricio, steve);
29 | }
30 |
31 | // you may want to add more tests here.
32 | }
--------------------------------------------------------------------------------
/ch6/src/test/java/ch6/stub/InvoiceFilterWithDatabaseTest.java:
--------------------------------------------------------------------------------
1 | package ch6.stub;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import static org.assertj.core.api.Assertions.assertThat;
8 |
9 | public class InvoiceFilterWithDatabaseTest {
10 | private IssuedInvoices invoices;
11 | private DatabaseConnection dbConnection;
12 |
13 | @BeforeEach
14 | public void open() {
15 | dbConnection = new DatabaseConnection();
16 | invoices = new IssuedInvoices(dbConnection);
17 |
18 | // we need to clean up all the tables,
19 | // to make sure old data doesn't interfere with the test.
20 | dbConnection.resetDatabase();
21 | }
22 |
23 | @AfterEach
24 | public void close() {
25 | if (dbConnection != null) dbConnection.close();
26 | }
27 |
28 | @Test
29 | void filterInvoices() {
30 | final var mauricio = new Invoice("Mauricio", 20);
31 | final var steve = new Invoice("Steve", 99);
32 | final var frank = new Invoice("Frank", 100);
33 |
34 | // really saves the invoice to the database...
35 | // this is no good.
36 | invoices.save(mauricio);
37 | invoices.save(steve);
38 | invoices.save(frank);
39 |
40 | final InvoiceFilterWithDatabase filter = new InvoiceFilterWithDatabase();
41 |
42 | assertThat(filter.lowValueInvoices())
43 | .containsExactlyInAnyOrder(mauricio, steve);
44 | }
45 | }
--------------------------------------------------------------------------------
/ch7/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch7
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 | org.hsqldb
20 | hsqldb
21 | 2.6.0
22 | test
23 |
24 |
25 |
26 |
27 | org.mockito
28 | mockito-junit-jupiter
29 | 3.12.1
30 | test
31 |
32 |
33 |
34 |
35 |
36 |
37 | org.assertj
38 | assertj-core
39 | 3.15.0
40 | test
41 |
42 |
43 |
44 |
45 | org.junit.jupiter
46 | junit-jupiter-engine
47 | 5.6.2
48 | test
49 |
50 |
51 |
52 |
53 | org.junit.jupiter
54 | junit-jupiter-params
55 | 5.6.2
56 | test
57 |
58 |
59 |
60 |
61 | org.mockito
62 | mockito-core
63 | 3.11.2
64 | test
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | maven-surefire-plugin
75 | 3.0.0-M5
76 |
77 | true
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/ch7/src/main/java/adapters/DeliveryCenterRestApi.java:
--------------------------------------------------------------------------------
1 | package adapters;
2 |
3 | import domain.ShoppingCart;
4 | import ports.DeliveryCenter;
5 |
6 | import java.time.LocalDate;
7 | import java.util.Calendar;
8 |
9 | public class DeliveryCenterRestApi implements DeliveryCenter {
10 | @Override
11 | public LocalDate deliver(ShoppingCart cart) {
12 | // all the code required to communicate
13 | // with the delivery API
14 | // and returns a LocalDate
15 | return null;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ch7/src/main/java/adapters/SAPSoapWebService.java:
--------------------------------------------------------------------------------
1 | package adapters;
2 |
3 | import domain.ShoppingCart;
4 | import ports.SAP;
5 |
6 | public class SAPSoapWebService implements SAP {
7 | @Override
8 | public void cartReadyForDelivery(ShoppingCart cart) {
9 | // all the code required to send the
10 | // cart to SAP's SOAP web service
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ch7/src/main/java/adapters/SMTPCustomerNotifier.java:
--------------------------------------------------------------------------------
1 | package adapters;
2 |
3 | import domain.ShoppingCart;
4 | import ports.CustomerNotifier;
5 |
6 | public class SMTPCustomerNotifier implements CustomerNotifier {
7 | @Override
8 | public void sendEstimatedDeliveryNotification(ShoppingCart cart) {
9 | // all the required code to
10 | // send an email via SMTP
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ch7/src/main/java/adapters/ShoppingCartHibernateDao.java:
--------------------------------------------------------------------------------
1 | package adapters;
2 |
3 |
4 | import domain.ShoppingCart;
5 | import ports.ShoppingCartRepository;
6 |
7 | import java.util.List;
8 |
9 | public class ShoppingCartHibernateDao implements ShoppingCartRepository {
10 | @Override
11 | public List cartsPaidToday() {
12 | // a hibernate query to get the list of all
13 | // invoices that were paid today
14 | return null;
15 | }
16 |
17 | @Override
18 | public void persist(ShoppingCart cart) {
19 | // a hibernate code to persist the cart
20 | // in the database
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ch7/src/main/java/domain/Installment.java:
--------------------------------------------------------------------------------
1 | package domain;
2 |
3 | import java.time.LocalDate;
4 | import java.util.Calendar;
5 |
6 | public class Installment {
7 |
8 | private final LocalDate date;
9 | private final double value;
10 |
11 | public Installment(LocalDate date, double value) {
12 | this.date = date;
13 | this.value = value;
14 | }
15 |
16 | public double getValue() {
17 | return value;
18 | }
19 |
20 | public LocalDate getDate() {
21 | return date;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ch7/src/main/java/domain/InstallmentGenerator.java:
--------------------------------------------------------------------------------
1 | package domain;
2 |
3 | import java.time.LocalDate;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | public class InstallmentGenerator {
8 |
9 | private InstallmentRepository repository;
10 |
11 | public InstallmentGenerator(InstallmentRepository repository) {
12 | this.repository = repository;
13 | }
14 |
15 | public void generateInstallments(ShoppingCart cart, int numberOfInstallments) {
16 | // create a variable that will store the last installment date
17 | LocalDate nextInstallmentDueDate = LocalDate.now();
18 |
19 | // calculate the amount per installment
20 | double amountPerInstallment = cart.getValue() / numberOfInstallments;
21 |
22 | // create a sequence of installments, one month apart from each other
23 | for(int i = 1; i <= numberOfInstallments; i++) {
24 | // +1 to the month
25 | nextInstallmentDueDate = nextInstallmentDueDate.plusMonths(1);
26 |
27 | // create and persist the installment
28 | Installment newInstallment = new Installment(nextInstallmentDueDate, amountPerInstallment);
29 | repository.persist(newInstallment);
30 | }
31 | }
32 |
33 | public List generateInstallments2(ShoppingCart cart, int numberOfInstallments) {
34 |
35 | List generatedInstallments = new ArrayList();
36 |
37 | // create a variable that will store the last installment date
38 | LocalDate nextInstallmentDueDate = LocalDate.now();
39 |
40 | // calculate the amount per installment
41 | double amountPerInstallment = cart.getValue() / numberOfInstallments;
42 |
43 | // create a sequence of installments, one month apart from each other
44 | for(int i = 1; i <= numberOfInstallments; i++) {
45 | // +1 to the month
46 | nextInstallmentDueDate = nextInstallmentDueDate.plusMonths(1);
47 |
48 | // create and persist the installment
49 | Installment newInstallment = new Installment(nextInstallmentDueDate, amountPerInstallment);
50 | generatedInstallments.add(newInstallment);
51 | repository.persist(newInstallment);
52 | }
53 |
54 | return generatedInstallments;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ch7/src/main/java/domain/InstallmentRepository.java:
--------------------------------------------------------------------------------
1 | package domain;
2 |
3 | public interface InstallmentRepository {
4 |
5 | void persist(Installment installment);
6 | }
7 |
--------------------------------------------------------------------------------
/ch7/src/main/java/domain/PaidShoppingCartsBatch.java:
--------------------------------------------------------------------------------
1 | package domain;
2 |
3 | import ports.DeliveryCenter;
4 | import ports.CustomerNotifier;
5 | import ports.SAP;
6 | import ports.ShoppingCartRepository;
7 |
8 | import java.time.LocalDate;
9 | import java.util.List;
10 |
11 | public class PaidShoppingCartsBatch {
12 |
13 | private ShoppingCartRepository db;
14 | private DeliveryCenter deliveryCenter;
15 | private CustomerNotifier notifier;
16 | private SAP sap;
17 |
18 | public PaidShoppingCartsBatch(ShoppingCartRepository db, DeliveryCenter deliveryCenter,
19 | CustomerNotifier notifier, SAP sap) {
20 | this.db = db;
21 | this.deliveryCenter = deliveryCenter;
22 | this.notifier = notifier;
23 | this.sap = sap;
24 | }
25 |
26 | public void processAll() {
27 |
28 | List paidShoppingCarts = db.cartsPaidToday();
29 | for (ShoppingCart cart : paidShoppingCarts) {
30 |
31 | // notify the delivery system about the delivery
32 | LocalDate estimatedDayOfDelivery = deliveryCenter.deliver(cart);
33 |
34 | // mark as ready for delivery and persist
35 | cart.markAsReadyForDelivery(estimatedDayOfDelivery);
36 | db.persist(cart);
37 |
38 | // send e-mail
39 | notifier.sendEstimatedDeliveryNotification(cart);
40 |
41 | // notify SAP
42 | sap.cartReadyForDelivery(cart);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ch7/src/main/java/domain/ShoppingCart.java:
--------------------------------------------------------------------------------
1 | package domain;
2 |
3 | import java.time.LocalDate;
4 | import java.util.Calendar;
5 |
6 | public class ShoppingCart {
7 | private boolean readyForDelivery = false;
8 | private double value = 0;
9 |
10 | public ShoppingCart(double value) {
11 | this.value = value;
12 | }
13 | // more info about the shopping cart...
14 |
15 | public void markAsReadyForDelivery(LocalDate estimatedDayOfDelivery) {
16 | this.readyForDelivery = true;
17 | // ...
18 | }
19 |
20 | public boolean isReadyForDelivery() {
21 | return readyForDelivery;
22 | }
23 |
24 | public double getValue() {
25 | return this.value;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ch7/src/main/java/domain/VeryBadPaidShoppingCartsBatch.java:
--------------------------------------------------------------------------------
1 | package domain;
2 |
3 |
4 | import adapters.DeliveryCenterRestApi;
5 | import adapters.SAPSoapWebService;
6 | import adapters.SMTPCustomerNotifier;
7 | import adapters.ShoppingCartHibernateDao;
8 |
9 | import java.time.LocalDate;
10 | import java.util.List;
11 |
12 | public class VeryBadPaidShoppingCartsBatch {
13 |
14 | public void processAll() {
15 |
16 | // we instantiate the db adapter.
17 | // Bad for testability!
18 | ShoppingCartHibernateDao db = new ShoppingCartHibernateDao();
19 | List paidShoppingCarts = db.cartsPaidToday();
20 | for (ShoppingCart cart : paidShoppingCarts) {
21 |
22 | // notify the delivery system about the delivery
23 | // but first, we need to instantiate its adapter.
24 | // Bad for testability!
25 | DeliveryCenterRestApi deliveryCenter = new DeliveryCenterRestApi();
26 | LocalDate estimatedDayOfDelivery = deliveryCenter.deliver(cart);
27 |
28 | // mark as ready for delivery and persist
29 | cart.markAsReadyForDelivery(estimatedDayOfDelivery);
30 | db.persist(cart);
31 |
32 | // send notification using the adapter directly
33 | // Bad for testability!
34 | SMTPCustomerNotifier notifier = new SMTPCustomerNotifier();
35 | notifier.sendEstimatedDeliveryNotification(cart);
36 |
37 | // notify SAP using the adapter directly
38 | // Bad for testability!
39 | SAPSoapWebService sap = new SAPSoapWebService();
40 | sap.cartReadyForDelivery(cart);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ch7/src/main/java/ports/CustomerNotifier.java:
--------------------------------------------------------------------------------
1 | package ports;
2 |
3 | import domain.ShoppingCart;
4 |
5 | public interface CustomerNotifier {
6 | void sendEstimatedDeliveryNotification(ShoppingCart cart);
7 | }
8 |
--------------------------------------------------------------------------------
/ch7/src/main/java/ports/DeliveryCenter.java:
--------------------------------------------------------------------------------
1 | package ports;
2 |
3 | import domain.ShoppingCart;
4 |
5 | import java.time.LocalDate;
6 | import java.util.Calendar;
7 |
8 | public interface DeliveryCenter {
9 | LocalDate deliver(ShoppingCart cart);
10 | }
11 |
--------------------------------------------------------------------------------
/ch7/src/main/java/ports/SAP.java:
--------------------------------------------------------------------------------
1 | package ports;
2 |
3 | import domain.ShoppingCart;
4 |
5 | public interface SAP {
6 | void cartReadyForDelivery(ShoppingCart cart);
7 | }
8 |
--------------------------------------------------------------------------------
/ch7/src/main/java/ports/ShoppingCartRepository.java:
--------------------------------------------------------------------------------
1 | package ports;
2 |
3 |
4 | import domain.ShoppingCart;
5 |
6 | import java.util.List;
7 |
8 | public interface ShoppingCartRepository {
9 | List cartsPaidToday();
10 | void persist(ShoppingCart cart);
11 | }
12 |
--------------------------------------------------------------------------------
/ch7/src/test/java/ch7/InstallmentGeneratorTest.java:
--------------------------------------------------------------------------------
1 | package ch7;
2 |
3 | import domain.Installment;
4 | import domain.InstallmentGenerator;
5 | import domain.InstallmentRepository;
6 | import domain.ShoppingCart;
7 | import org.junit.jupiter.api.Test;
8 | import org.junit.jupiter.api.extension.ExtendWith;
9 | import org.mockito.ArgumentCaptor;
10 | import org.mockito.Mock;
11 | import org.mockito.junit.jupiter.MockitoExtension;
12 |
13 | import java.time.LocalDate;
14 | import java.util.List;
15 |
16 | import static org.assertj.core.api.Assertions.assertThat;
17 | import static org.mockito.Mockito.times;
18 | import static org.mockito.Mockito.verify;
19 |
20 | @ExtendWith(MockitoExtension.class)
21 | public class InstallmentGeneratorTest {
22 |
23 | @Mock
24 | private InstallmentRepository repository;
25 |
26 | @Test
27 | void checkInstallments() {
28 |
29 | ShoppingCart cart = new ShoppingCart(100.0);
30 | InstallmentGenerator generator = new InstallmentGenerator(repository);
31 |
32 | generator.generateInstallments(cart, 10);
33 |
34 | // create a Mockito captor
35 | ArgumentCaptor captor = ArgumentCaptor.forClass(Installment.class);
36 |
37 | // get all the Installments that were passed to the repository
38 | verify(repository,times(10)).persist(captor.capture());
39 | List allInstallments = captor.getAllValues();
40 |
41 | // now, we assert that the installments are correct
42 | // all them should have a value of 10.0
43 | assertThat(allInstallments)
44 | .hasSize(10)
45 | .allMatch(i -> i.getValue() == 10);
46 |
47 | // they should have to be one month apart
48 | for(int month = 1; month <= 10; month++) {
49 | final LocalDate dueDate = LocalDate.now().plusMonths(month);
50 | assertThat(allInstallments)
51 | .anyMatch(i -> i.getDate().equals(dueDate));
52 | }
53 | }
54 |
55 | @Test
56 | void checkInstallments2() {
57 |
58 | ShoppingCart cart = new ShoppingCart(100.0);
59 | InstallmentGenerator generator = new InstallmentGenerator(repository);
60 |
61 | List allInstallments = generator.generateInstallments2(cart, 10);
62 |
63 | // now, we assert that the installments are correct
64 | // all them should have a value of 10.0
65 | assertThat(allInstallments)
66 | .hasSize(10)
67 | .allMatch(i -> i.getValue() == 10);
68 |
69 | // they should have to be one month apart
70 | for(int month = 1; month <= 10; month++) {
71 | final LocalDate dueDate = LocalDate.now().plusMonths(month);
72 | assertThat(allInstallments)
73 | .anyMatch(i -> i.getDate().equals(dueDate));
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ch7/src/test/java/ch7/PaidShoppingCartsBatchTest.java:
--------------------------------------------------------------------------------
1 | package ch7;
2 |
3 | import domain.PaidShoppingCartsBatch;
4 | import domain.ShoppingCart;
5 | import org.junit.jupiter.api.Test;
6 | import org.junit.jupiter.api.extension.ExtendWith;
7 | import org.mockito.Mock;
8 | import org.mockito.junit.jupiter.MockitoExtension;
9 | import ports.DeliveryCenter;
10 | import ports.CustomerNotifier;
11 | import ports.SAP;
12 | import ports.ShoppingCartRepository;
13 |
14 | import java.time.LocalDate;
15 | import java.util.Arrays;
16 |
17 | import static org.assertj.core.api.Assertions.assertThat;
18 | import static org.mockito.Mockito.*;
19 |
20 | @ExtendWith(MockitoExtension.class)
21 | public class PaidShoppingCartsBatchTest {
22 |
23 | @Mock
24 | ShoppingCartRepository db;
25 | @Mock private DeliveryCenter deliveryCenter;
26 | @Mock private CustomerNotifier notifier;
27 | @Mock private SAP sap;
28 |
29 | @Test
30 | void happyPath() {
31 | PaidShoppingCartsBatch batch = new PaidShoppingCartsBatch(db, deliveryCenter, notifier, sap);
32 |
33 | ShoppingCart someCart = new ShoppingCart(100);
34 | assertThat(someCart.isReadyForDelivery()).isFalse();
35 |
36 | LocalDate someDate = LocalDate.now();
37 | when(db.cartsPaidToday()).thenReturn(Arrays.asList(someCart));
38 | when(deliveryCenter.deliver(someCart)).thenReturn(someDate);
39 |
40 | batch.processAll();
41 |
42 | verify(deliveryCenter).deliver(someCart);
43 | verify(notifier).sendEstimatedDeliveryNotification(someCart);
44 | verify(db).persist(someCart);
45 | verify(sap).cartReadyForDelivery(someCart);
46 | assertThat(someCart.isReadyForDelivery()).isTrue();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ch8/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch8
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 |
20 | org.assertj
21 | assertj-core
22 | 3.15.0
23 | test
24 |
25 |
26 |
27 |
28 | org.junit.jupiter
29 | junit-jupiter-engine
30 | 5.6.2
31 | test
32 |
33 |
34 |
35 |
36 | org.junit.jupiter
37 | junit-jupiter-params
38 | 5.6.2
39 | test
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | maven-surefire-plugin
49 | 3.0.0-M5
50 |
51 | true
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/ch8/src/main/java/ch8/RomanNumeralConverter.java:
--------------------------------------------------------------------------------
1 | package ch8;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | public class RomanNumeralConverter {
7 | private static Map table =
8 | new HashMap() {{
9 | put('I', 1);
10 | put('V', 5);
11 | put('X', 10);
12 | put('L', 50);
13 | put('C', 100);
14 | put('D', 500);
15 | put('M', 1000);
16 | }};
17 |
18 | public int convert(String numberInRoman) {
19 | int finalNumber = 0;
20 | int lastNeighbor = 0;
21 | for(int i = numberInRoman.length() - 1; i >= 0; i--) {
22 |
23 | // get integer referent to current symbol
24 | int current = table.get(numberInRoman.charAt(i));
25 |
26 | // if right is lower, multiply it
27 | // by -1 to turn it negative
28 | int multiplier = 1;
29 | if(current < lastNeighbor) multiplier = -1;
30 |
31 | finalNumber += current * multiplier;
32 |
33 | // update neighbor at right
34 | lastNeighbor = current;
35 | }
36 | return finalNumber;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ch8/src/test/java/ch8/RomanNumeralConverterTest.java:
--------------------------------------------------------------------------------
1 | package ch8;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.CsvSource;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class RomanNumeralConverterTest {
9 |
10 | @ParameterizedTest
11 | @CsvSource({"I,1","V,5", "X,10","L,50", "C, 100", "D, 500", "M, 1000"})
12 | void shouldUnderstandOneCharNumbers(String romanNumeral, int expectedNumberAfterConversion) {
13 | RomanNumeralConverter roman = new RomanNumeralConverter();
14 | int convertedNumber = roman.convert(romanNumeral);
15 | assertThat(convertedNumber).isEqualTo(expectedNumberAfterConversion);
16 | }
17 |
18 | @ParameterizedTest
19 | @CsvSource({"II,2","III,3","VI, 6", "XVIII, 18", "XXIII, 23", "DCCLXVI, 766"})
20 | void shouldUnderstandMultipleCharNumbers(String romanNumeral, int expectedNumberAfterConversion) {
21 | RomanNumeralConverter roman = new RomanNumeralConverter();
22 | int convertedNumber = roman.convert(romanNumeral);
23 | assertThat(convertedNumber).isEqualTo(expectedNumberAfterConversion);
24 | }
25 |
26 | @ParameterizedTest
27 | @CsvSource({"IV,4","XIV,14", "XL, 40","XLI,41", "CCXCIV, 294"})
28 | void shouldUnderstandSubtractiveNotation(String romanNumeral, int expectedNumberAfterConversion) {
29 | RomanNumeralConverter roman = new RomanNumeralConverter();
30 | int convertedNumber = roman.convert(romanNumeral);
31 | assertThat(convertedNumber).isEqualTo(expectedNumberAfterConversion);
32 | }
33 |
34 | // if you prefer everything in a single test method
35 | @ParameterizedTest
36 | @CsvSource({
37 | // single character numbers
38 | "I,1","V,5", "X,10","L,50", "C, 100", "D, 500", "M, 1000",
39 | // multiple character numbers
40 | "II,2","III,3", "V,5","VI, 6", "XVIII, 18", "XXIII, 23", "DCCLXVI, 766",
41 | // subtractive notation
42 | "IV,4","XIV,14", "XL, 40","XLI,41", "CCXCIV, 294"
43 | })
44 | void convertRomanNumerals(String romanNumeral, int expectedNumberAfterConversion) {
45 | RomanNumeralConverter roman = new RomanNumeralConverter();
46 | int convertedNumber = roman.convert(romanNumeral);
47 | assertThat(convertedNumber).isEqualTo(expectedNumberAfterConversion);
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/ch9/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | ch9
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 |
20 | org.assertj
21 | assertj-core
22 | 3.15.0
23 | test
24 |
25 |
26 |
27 |
28 | org.junit.jupiter
29 | junit-jupiter-engine
30 | 5.6.2
31 | test
32 |
33 |
34 |
35 |
36 | org.junit.jupiter
37 | junit-jupiter-params
38 | 5.6.2
39 | test
40 |
41 |
42 |
43 |
44 | org.hsqldb
45 | hsqldb
46 | 2.6.0
47 | test
48 |
49 |
50 |
51 |
52 | org.mockito
53 | mockito-core
54 | 3.11.2
55 | test
56 |
57 |
58 |
59 |
60 | org.seleniumhq.selenium
61 | selenium-java
62 | 4.0.0-alpha-7
63 |
64 |
65 |
66 |
67 | org.seleniumhq.selenium
68 | selenium-safari-driver
69 | 4.0.0-alpha-7
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | maven-surefire-plugin
78 | 3.0.0-M5
79 |
80 | true
81 |
82 | **/system/*.java
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/ch9/spring-petclinic-2.5.0-SNAPSHOT.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/effective-software-testing/code/21429ccfdfc6e0fdb7c4227475306b6c26864096/ch9/spring-petclinic-2.5.0-SNAPSHOT.jar
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/DeliveryPrice.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | public class DeliveryPrice implements PriceRule {
4 | @Override
5 | public double priceToAggregate(ShoppingCart cart) {
6 |
7 | int totalItems = cart.numberOfItems();
8 |
9 | if(totalItems == 0)
10 | return 0;
11 | if(totalItems >= 1 && totalItems <= 3)
12 | return 5;
13 | if(totalItems >= 4 && totalItems <= 10)
14 | return 12.5;
15 |
16 | // for the tool to get 100% coverage, use the code below
17 | //if(totalItems == 0)
18 | // return 0;
19 | //if(totalItems <= 3)
20 | // return 5;
21 | //if(totalItems <= 10)
22 | // return 12.5;
23 |
24 | return 20.0;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/ExtraChargeForElectronics.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import java.util.List;
4 |
5 | public class ExtraChargeForElectronics implements PriceRule {
6 | @Override
7 | public double priceToAggregate(ShoppingCart cart) {
8 |
9 | List- items = cart.getItems();
10 |
11 | boolean hasAnElectronicDevice = items.stream().anyMatch(it -> it.getType() == ItemType.ELECTRONIC);
12 |
13 | if(hasAnElectronicDevice)
14 | return 7.50;
15 |
16 | return 0;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/FinalPriceCalculator.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import java.util.List;
4 |
5 | public class FinalPriceCalculator {
6 |
7 | private final List rules;
8 |
9 | public FinalPriceCalculator(List rules) {
10 | this.rules = rules;
11 | }
12 |
13 | public double calculate(ShoppingCart cart) {
14 | double finalPrice = 0;
15 |
16 | for (PriceRule rule : rules) {
17 | finalPrice += rule.priceToAggregate(cart);
18 | }
19 |
20 | return finalPrice;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/FinalPriceCalculatorFactory.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import java.util.Arrays;
4 | import java.util.List;
5 |
6 | public class FinalPriceCalculatorFactory {
7 |
8 | public FinalPriceCalculator build() {
9 | List priceRules = Arrays.asList(
10 | new PriceOfItems(),
11 | new ExtraChargeForElectronics(),
12 | new DeliveryPrice());
13 |
14 | return new FinalPriceCalculator(priceRules);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/Item.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | public class Item {
4 |
5 | private final ItemType type;
6 | private final String name;
7 | private final int quantity;
8 | private final double pricePerUnit;
9 |
10 | public Item(ItemType type, String name, int quantity, double pricePerUnit) {
11 | this.type = type;
12 | this.name = name;
13 | this.quantity = quantity;
14 | this.pricePerUnit = pricePerUnit;
15 | }
16 |
17 | public ItemType getType() {
18 | return type;
19 | }
20 |
21 | public String getName() {
22 | return name;
23 | }
24 |
25 | public int getQuantity() {
26 | return quantity;
27 | }
28 |
29 | public double getPricePerUnit() {
30 | return pricePerUnit;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/ItemType.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | public enum ItemType {
4 | ELECTRONIC,
5 | OTHER
6 | }
7 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/PriceOfItems.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import java.util.List;
4 |
5 | public class PriceOfItems implements PriceRule {
6 | @Override
7 | public double priceToAggregate(ShoppingCart cart) {
8 |
9 | double price = 0;
10 | List
- items = cart.getItems();
11 | for (Item item : items) {
12 | price += item.getPricePerUnit() * item.getQuantity();
13 | }
14 |
15 | return price;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/PriceRule.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | public interface PriceRule {
4 | double priceToAggregate(ShoppingCart cart);
5 | }
6 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/large/ShoppingCart.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collections;
5 | import java.util.List;
6 |
7 | public class ShoppingCart {
8 |
9 | private final List
- items = new ArrayList<>();
10 |
11 | public void add(Item item) {
12 | items.add(item);
13 | }
14 |
15 | public List
- getItems() {
16 | return Collections.unmodifiableList(items);
17 | }
18 |
19 | public int numberOfItems() {
20 | return items.size();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/sql/Invoice.java:
--------------------------------------------------------------------------------
1 | package ch9.sql;
2 |
3 | import java.util.Objects;
4 |
5 | public class Invoice {
6 | public final String customer;
7 | public final int value;
8 |
9 |
10 | public Invoice(String customer, int value) {
11 | this.customer = customer;
12 | this.value = value;
13 | }
14 |
15 | @Override
16 | public boolean equals(Object o) {
17 | if (this == o) return true;
18 | if (o == null || getClass() != o.getClass()) return false;
19 | Invoice invoice = (Invoice) o;
20 | return value == invoice.value &&
21 | customer.equals(invoice.customer);
22 | }
23 |
24 | @Override
25 | public int hashCode() {
26 | return Objects.hash(customer, value);
27 | }
28 |
29 | @Override
30 | public String toString() {
31 | return "Invoice{" +
32 | "customer='" + customer + '\'' +
33 | ", value=" + value +
34 | '}';
35 | }
36 |
37 | public int getValue() {
38 | return value;
39 | }
40 | }
--------------------------------------------------------------------------------
/ch9/src/main/java/ch9/sql/InvoiceDao.java:
--------------------------------------------------------------------------------
1 | package ch9.sql;
2 |
3 | import java.sql.*;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | public class InvoiceDao {
8 |
9 | private final Connection connection;
10 |
11 | public InvoiceDao(Connection connection) {
12 | this.connection = connection;
13 | }
14 |
15 | public List all() {
16 | try {
17 | PreparedStatement ps = connection.prepareStatement("select * from invoice");
18 |
19 | ResultSet rs = ps.executeQuery();
20 |
21 | List allInvoices = new ArrayList<>();
22 | while (rs.next()) {
23 | allInvoices.add(new Invoice(rs.getString("name"), rs.getInt("value")));
24 | }
25 |
26 | return allInvoices;
27 |
28 | } catch(Exception e) {
29 | throw new RuntimeException(e);
30 | }
31 | }
32 |
33 | public List allWithAtLeast(int value) {
34 | try {
35 | PreparedStatement ps = connection.prepareStatement("select * from invoice where value >= ?");
36 | ps.setInt(1, value);
37 | ResultSet rs = ps.executeQuery();
38 |
39 | List allInvoices = new ArrayList<>();
40 | while (rs.next()) {
41 | allInvoices.add(new Invoice(rs.getString("name"), rs.getInt("value")));
42 | }
43 | return allInvoices;
44 | } catch (Exception e) {
45 | throw new RuntimeException(e);
46 | }
47 | }
48 |
49 | public void save(Invoice inv) {
50 | try {
51 | PreparedStatement ps = connection.prepareStatement("insert into invoice (name, value) values (?,?)");
52 |
53 | ps.setString(1, inv.customer);
54 | ps.setInt(2, inv.value);
55 | ps.execute();
56 |
57 | connection.commit();
58 | } catch(Exception e) {
59 | throw new RuntimeException(e);
60 | }
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/large/DeliveryPriceTest.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.CsvSource;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class DeliveryPriceTest {
9 |
10 | @ParameterizedTest
11 | @CsvSource({"0,0",
12 | "1,5",
13 | "3,5",
14 | "4,12.5",
15 | "10,12.5",
16 | "11,20"})
17 | void deliveryIsAccordingToTheNumberOfItems(int noOfItems, double expectedDeliveryPrice) {
18 | ShoppingCart cart = new ShoppingCart();
19 | for(int i = 0; i < noOfItems; i++) {
20 | cart.add(new Item(ItemType.OTHER, "ANY", 1, 1));
21 | }
22 |
23 | double price = new DeliveryPrice().priceToAggregate(cart);
24 | assertThat(price).isEqualTo(expectedDeliveryPrice);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/large/ExtraChargeForElectronicsTest.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.junit.jupiter.params.ParameterizedTest;
5 | import org.junit.jupiter.params.provider.CsvSource;
6 |
7 | import static org.assertj.core.api.Assertions.assertThat;
8 |
9 | public class ExtraChargeForElectronicsTest {
10 |
11 | @ParameterizedTest
12 | @CsvSource({"1", "2"})
13 | void chargeTheExtraPriceIfThereIsAnyElectronicInTheCart(int numberOfElectronics) {
14 | ShoppingCart cart = new ShoppingCart();
15 | for(int i = 0; i < numberOfElectronics; i++) {
16 | cart.add(new Item(ItemType.ELECTRONIC, "ANY ELECTRONIC", 1, 1));
17 | }
18 |
19 | double price = new ExtraChargeForElectronics().priceToAggregate(cart);
20 | assertThat(price).isEqualTo(7.50);
21 | }
22 |
23 | @Test
24 | void noExtraChargesIfNoElectronics() {
25 | ShoppingCart cart = new ShoppingCart();
26 | cart.add(new Item(ItemType.OTHER, "BOOK", 1, 1));
27 | cart.add(new Item(ItemType.OTHER, "CD", 1, 1));
28 | cart.add(new Item(ItemType.OTHER, "BABY TOY", 1, 1));
29 |
30 | double price = new ExtraChargeForElectronics().priceToAggregate(cart);
31 | assertThat(price).isEqualTo(0);
32 | }
33 |
34 | @Test
35 | void noItems() {
36 | ShoppingCart cart = new ShoppingCart();
37 | double price = new ExtraChargeForElectronics().priceToAggregate(cart);
38 | assertThat(price).isEqualTo(0);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/large/FinalPriceCalculatorLargerTest.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | /**
8 | * Electronics:
9 | * - Has an item in the cart
10 | * - No items in the cart
11 | *
12 | * Delivery:
13 | * - 1 to 3 items
14 | * - 4 to 10 items
15 | * - More than 10 items
16 | *
17 | * Items:
18 | * - Empty
19 | * - 1 single element
20 | * - Many elements
21 | */
22 | public class FinalPriceCalculatorLargerTest {
23 |
24 | private final FinalPriceCalculator calculator = new FinalPriceCalculatorFactory().build();
25 |
26 | @Test
27 | void appliesAllRules() {
28 | ShoppingCart cart = new ShoppingCart();
29 | cart.add(new Item(ItemType.ELECTRONIC, "PS5", 1, 299));
30 | cart.add(new Item(ItemType.OTHER, "BOOK", 1, 29));
31 | cart.add(new Item(ItemType.OTHER, "CD", 2, 12));
32 | cart.add(new Item(ItemType.OTHER, "CHOCOLATE", 3, 1.50));
33 |
34 | double price = calculator.calculate(cart);
35 |
36 | double expectedPrice =
37 | 299 + 29 + 12 * 2 + 1.50 * 3 + // price of the items
38 | 7.50 + // has an electronic
39 | 12.5; // delivery price
40 |
41 | assertThat(price).isEqualTo(expectedPrice);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/large/FinalPriceCalculatorTest.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.Arrays;
6 | import java.util.List;
7 |
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.mockito.Mockito.mock;
10 | import static org.mockito.Mockito.when;
11 |
12 | public class FinalPriceCalculatorTest {
13 |
14 | @Test
15 | void callAllPriceRules() {
16 | PriceRule rule1 = mock(PriceRule.class);
17 | PriceRule rule2 = mock(PriceRule.class);
18 | PriceRule rule3 = mock(PriceRule.class);
19 |
20 | ShoppingCart cart = new ShoppingCart();
21 | cart.add(new Item(ItemType.OTHER, "ITEM", 1, 1));
22 |
23 | when(rule1.priceToAggregate(cart)).thenReturn(1.0);
24 | when(rule2.priceToAggregate(cart)).thenReturn(0.0);
25 | when(rule3.priceToAggregate(cart)).thenReturn(2.0);
26 |
27 | List rules = Arrays.asList(rule1, rule2, rule3);
28 | FinalPriceCalculator calculator = new FinalPriceCalculator(rules);
29 | double price = calculator.calculate(cart);
30 |
31 | assertThat(price).isEqualTo(3);
32 |
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/large/PriceOfItemsTest.java:
--------------------------------------------------------------------------------
1 | package ch9.large;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class PriceOfItemsTest {
8 |
9 | @Test
10 | void sumOfItems() {
11 | ShoppingCart cart = new ShoppingCart();
12 | cart.add(new Item(ItemType.ELECTRONIC, "PS5", 1, 299));
13 | cart.add(new Item(ItemType.OTHER, "GOD OF WAR (PS5)", 1, 59));
14 | cart.add(new Item(ItemType.OTHER, "BOOK", 1, 25));
15 | cart.add(new Item(ItemType.OTHER, "CD", 2, 12));
16 |
17 | double price = new PriceOfItems().priceToAggregate(cart);
18 |
19 | assertThat(price).isEqualTo(
20 | 1*299 + // PS5
21 | 1*59+ // game
22 | 1*25+ // book
23 | 2*12 // CD
24 | );
25 | }
26 |
27 | @Test
28 | void noItems() {
29 | ShoppingCart cart = new ShoppingCart();
30 | double price = new PriceOfItems().priceToAggregate(cart);
31 | assertThat(price).isEqualTo(0);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/sql/InvoiceDaoIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package ch9.sql;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import java.util.List;
6 |
7 | import static org.assertj.core.api.Assertions.assertThat;
8 |
9 | public class InvoiceDaoIntegrationTest extends SqlIntegrationTestBase {
10 |
11 |
12 | @Test
13 | void save() {
14 | Invoice inv1 = new Invoice("Mauricio", 10);
15 | Invoice inv2 = new Invoice("Frank", 11);
16 |
17 | dao.save(inv1);
18 |
19 | List afterSaving = dao.all();
20 | assertThat(afterSaving).containsExactlyInAnyOrder(inv1);
21 |
22 | dao.save(inv2);
23 | List afterSavingAgain = dao.all();
24 | assertThat(afterSavingAgain).containsExactlyInAnyOrder(inv1, inv2);
25 | }
26 |
27 | @Test
28 | void atLeast() {
29 | int value = 50;
30 |
31 | /**
32 | * Explore the boundary: value >= x
33 | * On point = x
34 | * Off point = x-1
35 | * In point = x + 1 (not really necessary, but it's cheap, and makes the
36 | * test strategy easier to comprehend)
37 | */
38 | Invoice inv1 = new Invoice("Mauricio", value - 1);
39 | Invoice inv2 = new Invoice("Arie", value);
40 | Invoice inv3 = new Invoice("Frank", value + 1);
41 |
42 | dao.save(inv1);
43 | dao.save(inv2);
44 | dao.save(inv3);
45 |
46 | List afterSaving = dao.allWithAtLeast(value);
47 | assertThat(afterSaving).containsExactlyInAnyOrder(inv2, inv3);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/sql/SqlIntegrationTestBase.java:
--------------------------------------------------------------------------------
1 | package ch9.sql;
2 |
3 | import org.junit.jupiter.api.AfterEach;
4 | import org.junit.jupiter.api.BeforeEach;
5 |
6 | import java.sql.Connection;
7 | import java.sql.DriverManager;
8 | import java.sql.PreparedStatement;
9 | import java.sql.SQLException;
10 |
11 | public class SqlIntegrationTestBase {
12 |
13 | private Connection connection;
14 | protected InvoiceDao dao;
15 |
16 | @BeforeEach
17 | void openConnectionAndCleanup() throws SQLException {
18 |
19 | connection = DriverManager.getConnection("jdbc:hsqldb:mem:book");
20 |
21 | /**
22 | * Create table if it doesn't exist. Only for the example here.
23 | */
24 | PreparedStatement preparedStatement = connection.prepareStatement("create table if not exists invoice (name varchar(100), value double)");
25 | preparedStatement.execute();
26 | connection.commit();
27 |
28 | dao = new InvoiceDao(connection);
29 |
30 | /**
31 | * Let's clean up the table before the test runs.
32 | * That will avoid possible flaky tests.
33 | *
34 | * Note that doing a single 'truncate' here seems simple and enough for this exercise.
35 | * In large systems, you will probably want to encapsulate the 'reset database' logic
36 | * somewhere else. Or even make use of specific frameworks for that.
37 | */
38 | connection.prepareStatement("truncate table invoice").execute();
39 | }
40 |
41 | @AfterEach
42 | void close() throws SQLException {
43 | /**
44 | * Closing up the connection might also be something you do
45 | * at the end of each test.
46 | * Or maybe only at the end of the entire test suite, just to optimize.
47 | * (In practice, you should also use some connection pool, like C3P0,
48 | * to handle connections)
49 | */
50 | connection.close();
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/FindOwnersFlowTest.java:
--------------------------------------------------------------------------------
1 | package ch9.system;
2 |
3 | import ch9.system.pages.*;
4 | import org.junit.jupiter.api.AfterAll;
5 | import org.junit.jupiter.api.Test;
6 | import org.openqa.selenium.WebDriver;
7 | import org.openqa.selenium.safari.SafariDriver;
8 |
9 | import java.util.List;
10 |
11 | import static org.assertj.core.api.Assertions.assertThat;
12 |
13 | public class FindOwnersFlowTest {
14 | private FindOwnersPage page = new FindOwnersPage(driver);
15 |
16 | protected static WebDriver driver = new SafariDriver();
17 |
18 | @AfterAll
19 | static void close() {
20 | driver.close();
21 | }
22 |
23 | @Test
24 | void findOwnersBasedOnTheirLastNames() {
25 | AddOwnerInfo owner1 = new AddOwnerInfo("John", "Doe", "some address", "some city", "11111");
26 | AddOwnerInfo owner2 = new AddOwnerInfo("Jane", "Doe", "some address", "some city", "11111");
27 | addOwners(owner1, owner2);
28 |
29 | page.visit();
30 |
31 | ListOfOwnersPage listPage = page.findOwners("Doe");
32 | List all = listPage.all();
33 |
34 | assertThat(all).hasSize(2).
35 | containsExactlyInAnyOrder(owner1.toOwnerInfo(), owner2.toOwnerInfo());
36 | }
37 |
38 | private void addOwners(AddOwnerInfo... owners) {
39 | AddOwnerPage addOwnerPage = new AddOwnerPage(driver);
40 |
41 | for (AddOwnerInfo owner : owners) {
42 | addOwnerPage.visit();
43 | addOwnerPage.add(owner);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/FirstSeleniumTest.java:
--------------------------------------------------------------------------------
1 | package ch9.system;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.openqa.selenium.By;
5 | import org.openqa.selenium.WebDriver;
6 | import org.openqa.selenium.WebElement;
7 | import org.openqa.selenium.safari.SafariDriver;
8 |
9 | import static org.assertj.core.api.Assertions.assertThat;
10 |
11 | public class FirstSeleniumTest {
12 |
13 | @Test
14 | void firstSeleniumTest() {
15 | // select which driver to use
16 | WebDriver browser = new SafariDriver();
17 |
18 | // visit a page
19 | browser.get("http://localhost:8080");
20 |
21 | // find an HTML element in the page
22 | WebElement welcomeHeader = browser.findElement(By.tagName("h2"));
23 |
24 | // assert it contains what we want
25 | assertThat(welcomeHeader.getText()).isEqualTo("Welcome");
26 |
27 | // close the browser and the selenium session
28 | browser.close();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/WebTests.java:
--------------------------------------------------------------------------------
1 | package ch9.system;
2 |
3 | import org.junit.jupiter.api.AfterAll;
4 | import org.junit.jupiter.api.BeforeAll;
5 | import org.openqa.selenium.WebDriver;
6 | import org.openqa.selenium.safari.SafariDriver;
7 |
8 | public class WebTests {
9 |
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/AddOwnerInfo.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | public class AddOwnerInfo {
4 |
5 | private String firstName;
6 | private String lastName;
7 | private String address;
8 | private String city;
9 | private String telephone;
10 |
11 | public AddOwnerInfo(String firstName, String lastName, String address, String city, String telephone) {
12 | this.firstName = firstName;
13 | this.lastName = lastName;
14 | this.address = address;
15 | this.city = city;
16 | this.telephone = telephone;
17 | }
18 |
19 | public String getFirstName() {
20 | return firstName;
21 | }
22 |
23 | public String getLastName() {
24 | return lastName;
25 | }
26 |
27 | public String getAddress() {
28 | return address;
29 | }
30 |
31 | public String getCity() {
32 | return city;
33 | }
34 |
35 | public String getTelephone() {
36 | return telephone;
37 | }
38 |
39 | public OwnerInfo toOwnerInfo() {
40 | return new OwnerInfo(firstName + " " + lastName, address, city, telephone, "");
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/AddOwnerPage.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | import org.openqa.selenium.By;
4 | import org.openqa.selenium.WebDriver;
5 | import org.openqa.selenium.support.ui.ExpectedConditions;
6 | import org.openqa.selenium.support.ui.WebDriverWait;
7 |
8 | import java.time.Duration;
9 |
10 | public class AddOwnerPage extends PetClinicPageObject {
11 | public AddOwnerPage(WebDriver driver) {
12 | super(driver);
13 | }
14 |
15 | @Override
16 | public void visit() {
17 | visit("/owners/new");
18 | }
19 |
20 | @Override
21 | public void isReady() {
22 | WebDriverWait wait = new WebDriverWait (driver, Duration.ofSeconds(3));
23 | wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("add-owner-form")));
24 | }
25 |
26 | public OwnerInformationPage add(AddOwnerInfo ownerToBeAdded) {
27 | driver.findElement(By.id("firstName")).sendKeys(ownerToBeAdded.getFirstName());
28 | driver.findElement(By.id("lastName")).sendKeys(ownerToBeAdded.getLastName());
29 | driver.findElement(By.id("address")).sendKeys(ownerToBeAdded.getAddress());
30 | driver.findElement(By.id("city")).sendKeys(ownerToBeAdded.getCity());
31 | driver.findElement(By.id("telephone")).sendKeys(ownerToBeAdded.getTelephone());
32 |
33 | driver.findElement(By.id("add-owner-form"))
34 | .findElement(By.tagName("button"))
35 | .click();
36 |
37 |
38 | OwnerInformationPage ownerInformationPage = new OwnerInformationPage(driver);
39 | ownerInformationPage.isReady();
40 | return ownerInformationPage;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/FindOwnersPage.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | import org.openqa.selenium.By;
4 | import org.openqa.selenium.WebDriver;
5 | import org.openqa.selenium.WebElement;
6 | import org.openqa.selenium.support.ui.ExpectedConditions;
7 | import org.openqa.selenium.support.ui.WebDriverWait;
8 |
9 | import java.time.Duration;
10 | import java.util.Optional;
11 |
12 | public class FindOwnersPage extends PetClinicPageObject {
13 |
14 | public FindOwnersPage(WebDriver driver) {
15 | super(driver);
16 | }
17 |
18 | public ListOfOwnersPage findOwners(String ownerLastName) {
19 |
20 | driver.findElement(By.id("lastName")).sendKeys(ownerLastName);
21 | WebElement findOwnerButton = driver.findElement(By.id("search-owner-form")).findElement(By.tagName("button"));
22 | findOwnerButton.click();
23 |
24 | ListOfOwnersPage listOfOwnersPage = new ListOfOwnersPage(driver);
25 | listOfOwnersPage.isReady();
26 | return listOfOwnersPage;
27 | }
28 |
29 | public AddOwnerPage addOwner() {
30 | Optional link = driver.findElements(By.tagName("a"))
31 | .stream().filter(el -> el.getText().equals("Add Owner")).findFirst();
32 | link.get().click();
33 |
34 | AddOwnerPage addOwnerPage = new AddOwnerPage(driver);
35 | addOwnerPage.isReady();
36 | return addOwnerPage;
37 | }
38 |
39 | public void visit() {
40 | visit("/owners/find");
41 | }
42 |
43 | @Override
44 | public void isReady() {
45 | WebDriverWait wait = new WebDriverWait (driver, Duration.ofSeconds(3));
46 | wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("search-owner-form")));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/ListOfOwnersPage.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | import org.openqa.selenium.By;
4 | import org.openqa.selenium.WebDriver;
5 | import org.openqa.selenium.WebElement;
6 | import org.openqa.selenium.support.ui.ExpectedConditions;
7 | import org.openqa.selenium.support.ui.WebDriverWait;
8 |
9 | import java.time.Duration;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 |
13 | public class ListOfOwnersPage extends PetClinicPageObject{
14 | public ListOfOwnersPage(WebDriver driver) {
15 | super(driver);
16 | }
17 |
18 | @Override
19 | public void isReady() {
20 | WebDriverWait wait = new WebDriverWait (driver, Duration.ofSeconds(3));
21 | wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("owners")));
22 | }
23 |
24 | public List all() {
25 | List owners = new ArrayList<>();
26 |
27 | WebElement table = driver.findElement(By.id("owners"));
28 | List rows = table.findElement(By.tagName("tbody")).findElements(By.tagName("tr"));
29 |
30 | for (WebElement row : rows) {
31 | List columns = row.findElements(By.tagName("td"));
32 |
33 | String name = columns.get(0).getText().trim();
34 | String address = columns.get(1).getText().trim();
35 | String city = columns.get(2).getText().trim();
36 | String telephone = columns.get(3).getText().trim();
37 | String pets = columns.get(4).getText().trim();
38 |
39 | OwnerInfo ownerInfo = new OwnerInfo(name, address, city, telephone, pets);
40 | owners.add(ownerInfo);
41 | }
42 |
43 | return owners;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/OwnerInfo.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | import java.util.Objects;
4 |
5 | public class OwnerInfo {
6 | private String name;
7 | private String address;
8 | private String city;
9 | private String telephone;
10 | private String pets;
11 |
12 | public OwnerInfo(String name, String address, String city, String telephone, String pets) {
13 | this.name = name;
14 | this.address = address;
15 | this.city = city;
16 | this.telephone = telephone;
17 | this.pets = pets;
18 | }
19 |
20 | public String getName() {
21 | return name;
22 | }
23 |
24 | public String getAddress() {
25 | return address;
26 | }
27 |
28 | public String getCity() {
29 | return city;
30 | }
31 |
32 | public String getTelephone() {
33 | return telephone;
34 | }
35 |
36 | public String getPets() {
37 | return pets;
38 | }
39 |
40 | @Override
41 | public boolean equals(Object o) {
42 | if (this == o) return true;
43 | if (o == null || getClass() != o.getClass()) return false;
44 | OwnerInfo ownerInfo = (OwnerInfo) o;
45 | return Objects.equals(name, ownerInfo.name) &&
46 | Objects.equals(address, ownerInfo.address) &&
47 | Objects.equals(city, ownerInfo.city) &&
48 | Objects.equals(telephone, ownerInfo.telephone) &&
49 | Objects.equals(pets, ownerInfo.pets);
50 | }
51 |
52 | @Override
53 | public int hashCode() {
54 | return Objects.hash(name, address, city, telephone, pets);
55 | }
56 |
57 | @Override
58 | public String toString() {
59 | return "OwnerInfo{" +
60 | "name='" + name + '\'' +
61 | ", address='" + address + '\'' +
62 | ", city='" + city + '\'' +
63 | ", telephone='" + telephone + '\'' +
64 | ", pets='" + pets + '\'' +
65 | '}';
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/OwnerInformationPage.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | import org.openqa.selenium.By;
4 | import org.openqa.selenium.WebDriver;
5 | import org.openqa.selenium.support.ui.ExpectedConditions;
6 | import org.openqa.selenium.support.ui.WebDriverWait;
7 |
8 | import java.time.Duration;
9 |
10 | public class OwnerInformationPage extends PetClinicPageObject {
11 | public OwnerInformationPage(WebDriver driver) {
12 | super(driver);
13 | }
14 |
15 | @Override
16 | public void isReady() {
17 | WebDriverWait wait = new WebDriverWait (driver, Duration.ofSeconds(3));
18 | wait.until(ExpectedConditions.textToBe(By.tagName("h2"), "Owner Information"));
19 | }
20 |
21 | // fully represent the page here...
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/ch9/src/test/java/ch9/system/pages/PetClinicPageObject.java:
--------------------------------------------------------------------------------
1 | package ch9.system.pages;
2 |
3 | import org.openqa.selenium.WebDriver;
4 |
5 | public abstract class PetClinicPageObject {
6 |
7 | protected final WebDriver driver;
8 |
9 | public PetClinicPageObject(WebDriver driver) {
10 | this.driver = driver;
11 | }
12 |
13 | public void visit() {
14 | throw new RuntimeException("This page does not have a visit link");
15 | }
16 |
17 | protected void visit(String url) {
18 | driver.get("http://localhost:8080" + url);
19 | isReady();
20 | }
21 |
22 | public abstract void isReady();
23 | }
24 |
--------------------------------------------------------------------------------
/intro-to-junit/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | aniche-testing-for-developers
8 | appendix
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | org.assertj
22 | assertj-core
23 | 3.15.0
24 | test
25 |
26 |
27 |
28 |
29 | org.junit.jupiter
30 | junit-jupiter-engine
31 | 5.6.2
32 | test
33 |
34 |
35 |
36 |
37 | org.junit.jupiter
38 | junit-jupiter-params
39 | 5.6.2
40 | test
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | maven-surefire-plugin
50 | 3.0.0-M5
51 |
52 | true
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/intro-to-junit/src/main/java/appendix/BlockCounter.java:
--------------------------------------------------------------------------------
1 | package appendix;
2 |
3 | public class BlockCounter {
4 |
5 | public int maxBlock(String str) {
6 | if(str.isEmpty())
7 | return 0;
8 |
9 | int largestBlockSize = 1;
10 | int currentBlockSize = 1;
11 | char lastChar = str.charAt(0);
12 |
13 | for(int i = 1; i < str.length(); i++) {
14 | char currentChar = str.charAt(i);
15 |
16 | currentBlockSize = (currentChar == lastChar ? currentBlockSize+1 : 1);
17 | lastChar = currentChar;
18 | largestBlockSize = Math.max(largestBlockSize, currentBlockSize);
19 | }
20 |
21 | return largestBlockSize;
22 | }
23 |
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/intro-to-junit/src/test/java/appendix/BlockCounterParameterizedTest.java:
--------------------------------------------------------------------------------
1 | package appendix;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.CsvSource;
5 | import static org.assertj.core.api.Assertions.assertThat;
6 |
7 | public class BlockCounterParameterizedTest {
8 |
9 | @ParameterizedTest
10 | @CsvSource({
11 | "aabbbcc,3",
12 | "aabbbccc,3",
13 | "abc,1",
14 | "aa,2",
15 | "aabbbb,4",
16 | "'',0"
17 | })
18 | void countTheNumberOfMaxBlocks(String input, int expectedOutput) {
19 | int blockSize = new BlockCounter().maxBlock(input);
20 | assertThat(blockSize).isEqualTo(expectedOutput);
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/intro-to-junit/src/test/java/appendix/BlockCounterParameterizedTest2.java:
--------------------------------------------------------------------------------
1 | package appendix;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.Arguments;
5 | import org.junit.jupiter.params.provider.MethodSource;
6 | import java.util.stream.Stream;
7 | import static org.assertj.core.api.Assertions.assertThat;
8 |
9 | public class BlockCounterParameterizedTest2 {
10 |
11 | @ParameterizedTest
12 | @MethodSource("inputs")
13 | void countTheNumberOfMaxBlocks(String input, int expectedOutput) {
14 | int blockSize = new BlockCounter().maxBlock(input);
15 | assertThat(blockSize).isEqualTo(expectedOutput);
16 | }
17 |
18 | static Stream inputs() {
19 | return Stream.of(
20 | Arguments.of("aabbbcc", 3),
21 | Arguments.of("aabbbccc", 3),
22 | Arguments.of("abc", 1),
23 | Arguments.of("aa", 2),
24 | Arguments.of("aabbbb", 4),
25 | Arguments.of("", 0)
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/intro-to-junit/src/test/java/appendix/BlockCounterTest.java:
--------------------------------------------------------------------------------
1 | package appendix;
2 |
3 | import org.junit.jupiter.api.Test;
4 |
5 | import static org.assertj.core.api.Assertions.assertThat;
6 | import static org.junit.jupiter.api.Assertions.assertEquals;
7 |
8 | public class BlockCounterTest {
9 |
10 | @Test
11 | void oneLongestBlock() {
12 | int blockSize = new BlockCounter().maxBlock("aabbbcc");
13 | assertThat(blockSize).isEqualTo(3);
14 | }
15 |
16 | @Test
17 | void twoLongestBlocks() {
18 | int blockSize = new BlockCounter().maxBlock("aabbbccc");
19 | assertThat(blockSize).isEqualTo(3);
20 | }
21 |
22 | @Test
23 | void blockOfLength1() {
24 | int blockSize = new BlockCounter().maxBlock("abc");
25 | assertThat(blockSize).isEqualTo(1);
26 | }
27 |
28 | // this one finds the bug!
29 | @Test
30 | void singleBlock() {
31 | int blockSize = new BlockCounter().maxBlock("aa");
32 | assertThat(blockSize).isEqualTo(2);
33 | }
34 |
35 | @Test
36 | void longestBlockIsTheLast() {
37 | int blockSize = new BlockCounter().maxBlock("aabbbb");
38 | assertThat(blockSize).isEqualTo(4);
39 | }
40 |
41 | // finds the second bug
42 | @Test
43 | void emptyString() {
44 | int blockSize = new BlockCounter().maxBlock("");
45 | assertThat(blockSize).isEqualTo(0);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/intro-to-junit/src/test/java/appendix/BlockCounterWithBeforeAndAfterTest.java:
--------------------------------------------------------------------------------
1 | package appendix;
2 |
3 | import org.junit.jupiter.api.BeforeEach;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import static org.assertj.core.api.Assertions.assertThat;
7 |
8 | public class BlockCounterWithBeforeAndAfterTest {
9 |
10 | private BlockCounter blockCounter;
11 |
12 | @BeforeEach
13 | void setup() {
14 | this.blockCounter = new BlockCounter();
15 | }
16 |
17 | @Test
18 | void oneLongestBlock() {
19 | blockCounter = new BlockCounter();
20 | int blockSize = blockCounter.maxBlock("aabbbcc");
21 | assertThat(blockSize).isEqualTo(3);
22 | }
23 |
24 | @Test
25 | void twoLongestBlocks() {
26 | int blockSize = blockCounter.maxBlock("aabbbccc");
27 | assertThat(blockSize).isEqualTo(3);
28 | }
29 |
30 | @Test
31 | void blockOfLength1() {
32 | int blockSize = blockCounter.maxBlock("abc");
33 | assertThat(blockSize).isEqualTo(1);
34 | }
35 |
36 | @Test
37 | void singleBlock() {
38 | int blockSize = blockCounter.maxBlock("aa");
39 | assertThat(blockSize).isEqualTo(2);
40 | }
41 |
42 | @Test
43 | void longestBlockIsTheLast() {
44 | int blockSize = blockCounter.maxBlock("aabbbb");
45 | assertThat(blockSize).isEqualTo(4);
46 | }
47 |
48 | @Test
49 | void emptyString() {
50 | int blockSize = blockCounter.maxBlock("");
51 | assertThat(blockSize).isEqualTo(0);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------