├── .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 | ![example workflow](https://github.com/effective-software-testing/code/actions/workflows/tests.yml/badge.svg) 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 | --------------------------------------------------------------------------------