├── .github └── workflows │ ├── ci.yml │ └── dependabot-workflow.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── examples ├── build.gradle └── src │ └── test │ └── java │ └── com │ └── zendesk │ └── jazon │ └── junit │ ├── ExampleTest.java │ ├── ExamplesWithGuavaTest.java │ └── ReadmeExamplesTest.java ├── gradle.properties ├── gradle ├── publishing.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jazon-core ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── zendesk │ │ └── jazon │ │ ├── MatchResult.java │ │ ├── Matcher.java │ │ ├── MatcherFactory.java │ │ ├── actual │ │ ├── Actual.java │ │ ├── ActualJsonArray.java │ │ ├── ActualJsonBoolean.java │ │ ├── ActualJsonNull.java │ │ ├── ActualJsonNumber.java │ │ ├── ActualJsonObject.java │ │ ├── ActualJsonString.java │ │ └── factory │ │ │ ├── ActualFactory.java │ │ │ └── GsonActualFactory.java │ │ ├── expectation │ │ ├── Expectations.java │ │ ├── JsonExpectation.java │ │ ├── impl │ │ │ ├── AnyNumberOf.java │ │ │ ├── AnyNumberOfExpectation.java │ │ │ ├── NullExpectation.java │ │ │ ├── ObjectExpectation.java │ │ │ ├── OrderedArrayExpectation.java │ │ │ ├── PredicateExpectation.java │ │ │ ├── PrimitiveValueExpectation.java │ │ │ └── UnorderedArrayExpectation.java │ │ └── translator │ │ │ ├── DefaultTranslators.java │ │ │ ├── JazonTypesTranslators.java │ │ │ ├── Translator.java │ │ │ ├── TranslatorFacade.java │ │ │ └── TranslatorMapping.java │ │ ├── mismatch │ │ ├── Mismatch.java │ │ ├── MismatchWithPath.java │ │ ├── MismatchWithPathFactory.java │ │ └── impl │ │ │ ├── ArrayLackingElementsMismatch.java │ │ │ ├── ArrayUnexpectedElementsMismatch.java │ │ │ ├── NoFieldMismatch.java │ │ │ ├── NotNullMismatch.java │ │ │ ├── NullMismatch.java │ │ │ ├── PredicateExecutionFailedMismatch.java │ │ │ ├── PredicateMismatch.java │ │ │ ├── PrimitiveValueMismatch.java │ │ │ ├── TypeMismatch.java │ │ │ └── UnexpectedFieldMismatch.java │ │ └── util │ │ └── Preconditions.java │ └── test │ ├── groovy │ └── com │ │ └── zendesk │ │ └── jazon │ │ ├── MatcherSpec.groovy │ │ ├── MessagesSpec.groovy │ │ ├── MismatchPathSpec.groovy │ │ ├── actual │ │ └── ActualJsonNumberSpec.groovy │ │ └── mismatch │ │ └── TypeMismatchSpec.groovy │ └── java │ └── com │ └── zendesk │ └── jazon │ └── TestActualFactory.java ├── jazon-junit ├── README.md ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── zendesk │ │ └── jazon │ │ ├── expectation │ │ └── translator │ │ │ └── JunitTranslators.java │ │ └── junit │ │ ├── JazonJunitAdapter.java │ │ ├── JazonList.java │ │ ├── JazonMap.java │ │ ├── JsonExpectationInput.java │ │ ├── ObjectExpectationInput.java │ │ └── PredicateExpectationInput.java │ └── test │ └── java │ └── com │ └── zendesk │ └── jazon │ └── junit │ ├── JazonListTest.java │ ├── JazonMapTest.java │ └── JunitSpecificMatcherTest.java ├── jazon-spock ├── README.md ├── build.gradle └── src │ ├── main │ ├── groovy │ │ └── com │ │ │ └── zendesk │ │ │ └── jazon │ │ │ └── spock │ │ │ └── JazonSpockAdapter.groovy │ └── java │ │ └── com │ │ └── zendesk │ │ └── jazon │ │ └── expectation │ │ └── translator │ │ └── SpockTranslators.java │ └── test │ └── groovy │ └── com │ └── zendesk │ └── jazon │ ├── ExampleSpec.groovy │ ├── MatcherForGroovySpec.groovy │ ├── MessagesForGroovySpec.groovy │ └── spock │ └── JazonSpockAdapterSpec.groovy └── settings.gradle /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-22.04 14 | strategy: 15 | matrix: 16 | java: ['8'] 17 | steps: 18 | - uses: zendesk/checkout@v2 19 | - name: Set up JDK 20 | uses: zendesk/setup-java@v1 21 | with: 22 | java-version: ${{ matrix.java }} 23 | - name: print Java version 24 | run: java -version 25 | - name: Build 26 | run: ./gradlew clean build 27 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Update dependency graph 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | update-dependency-graph: 12 | runs-on: [self-hosted, zendesk-stable] 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4.2.2 16 | 17 | - name: Set up JDK 18 | uses: actions/setup-java@v4.7.0 19 | 20 | with: 21 | distribution: 'corretto' 22 | java-version: '17' 23 | 24 | - name: Fix maven-repo.gradle 25 | run: | 26 | # Display content of the problematic file 27 | echo "Current content of maven-repo.gradle:" 28 | cat gradle/maven-repo.gradle 29 | 30 | # Create a temporary file with fixes 31 | cat > gradle/maven-repo.gradle.new << 'EOL' 32 | // Define the property as an extension property 33 | ext { 34 | baseMavenRepoAllUrl = System.getenv('BASE_MAVEN_REPO_ALL_URL') ?: "https://zdrepo.jfrog.io/zdrepo/zen-libs-m2/" 35 | } 36 | 37 | // Rest of the original file content can follow 38 | // ... keep any other needed content from the original file 39 | EOL 40 | 41 | # Replace the original file 42 | mv gradle/maven-repo.gradle.new gradle/maven-repo.gradle 43 | 44 | # Verify the change 45 | echo "Updated content of maven-repo.gradle:" 46 | cat gradle/maven-repo.gradle 47 | 48 | - name: Print Gradle info 49 | run: | 50 | ./gradlew --version 51 | ./gradlew properties 52 | 53 | - name: Generate and submit dependency graph 54 | uses: gradle/actions/dependency-submission@v4.3.0 55 | with: 56 | gradle-build-root: . 57 | gradle-build-module: . 58 | gradle-build-configuration: implementation 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ IDEA 2 | .idea/ 3 | *.iml 4 | *.iws 5 | *.ipr 6 | .ideaDataSources/ 7 | 8 | # Eclipse 9 | .classpath 10 | .project 11 | .settings/ 12 | 13 | # Gradle 14 | build/ 15 | .gradle/ 16 | .gradletasknamecache 17 | 18 | # Misc 19 | bin/ 20 | log/ 21 | *.log 22 | out/ 23 | .DS_Store 24 | 25 | # Does not ignore anything is source folders 26 | !**/src/** 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | script: ./gradlew clean build 3 | deploy: 4 | provider: script 5 | script: ./gradlew publish -PossrhUsername=${SECRET_OSSRH_USERNAME} -PossrhPassword=${SECRET_OSSRH_PASSWORD} 6 | skip_cleanup: true 7 | on: 8 | repo: zendesk/jazon 9 | all_branches: true 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jazon 2 | ![CI status](https://github.com/zendesk/jazon/actions/workflows/ci.yml/badge.svg) 3 | 4 | A library for test assertions on JSON payloads. 5 | 6 | Supports Spock and JUnit. Easy to extend for other test frameworks and languages. 7 | 8 | ## About 9 | 10 | Jazon was created to make writing tests on JSON APIs easy. It offers: 11 | * Simple exact-match assertions on JSON 12 | * Matching unordered JSON arrays (ability to ignore the items order) 13 | * User-defined wildcard assertions, e.g. 14 | * Match string to a regex 15 | * Match number to a range 16 | * Match datetime to some specific format 17 | * Verify that float has only 2 decimal points 18 | * Verify that list has only 25 items 19 | * ... anything you need 20 | * Human-readable error messages for fast mismatch tracing 21 | * Optimised to minimise code duplication 22 | 23 | ## Using Jazon in your project 24 | 25 | Jazon is provided as separate libraries (so called adapters) for each supported testing framework. 26 | Depending on the framework you use, pick the adapter library that is applicable for you. 27 | 28 | ### Spock 29 | 30 | [User guide for Spock Adapter](jazon-spock/README.md#Quickstart) 31 | 32 | ##### Gradle: 33 | ```groovy 34 | dependencies { 35 | testCompile 'com.zendesk.jazon:jazon-spock:0.4.1' 36 | } 37 | ``` 38 | ##### Maven: 39 | ```xml 40 | 41 | com.zendesk.jazon 42 | jazon-spock 43 | 0.4.1 44 | test 45 | 46 | ``` 47 | 48 | ### JUnit: 49 | 50 | [User guide for JUnit Adapter](jazon-junit/README.md#Quickstart) 51 | 52 | ##### Gradle: 53 | ```groovy 54 | dependencies { 55 | testCompile 'com.zendesk.jazon:jazon-junit:0.4.1' 56 | } 57 | ``` 58 | ##### Maven: 59 | ```xml 60 | 61 | com.zendesk.jazon 62 | jazon-junit 63 | 0.4.1 64 | test 65 | 66 | ``` 67 | 68 | ## Copyright and license 69 | Copyright 2019 Zendesk, Inc. 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 72 | You may obtain a copy of the License at 73 | http://www.apache.org/licenses/LICENSE-2.0 74 | 75 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 76 | 77 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | group 'com.zendesk.jazon' 3 | } 4 | 5 | subprojects { 6 | apply plugin: 'groovy' 7 | 8 | sourceCompatibility = 1.8 9 | 10 | repositories { 11 | jcenter() 12 | } 13 | 14 | task sourceJar(type: Jar, dependsOn: classes) { 15 | classifier 'sources' 16 | from sourceSets.main.allSource 17 | } 18 | 19 | task javadocJar(type: Jar) { 20 | classifier = 'javadoc' 21 | from javadoc 22 | } 23 | 24 | artifacts { 25 | archives sourceJar, javadocJar 26 | } 27 | 28 | apply plugin: 'signing' 29 | apply plugin: 'maven-publish' 30 | 31 | signing { 32 | required { !isSnapshotVersion() } 33 | sign publishing.publications 34 | } 35 | 36 | publishing { 37 | repositories { 38 | maven { 39 | String snapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' 40 | String releaseUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' 41 | url = isSnapshotVersion() ? snapshotUrl : releaseUrl 42 | credentials { 43 | username project.findProperty('ossrhUsername') ?: '' 44 | password project.findProperty('ossrhPassword') ?: '' 45 | } 46 | } 47 | } 48 | } 49 | 50 | publish { 51 | doLast { 52 | println "The published version: ${version}" 53 | } 54 | } 55 | } 56 | 57 | boolean isSnapshotVersion() { 58 | version.endsWith('SNAPSHOT') 59 | } 60 | -------------------------------------------------------------------------------- /examples/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | testImplementation project(':jazon-junit') 3 | testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.1' 4 | testImplementation group: 'com.google.guava', name: 'guava', version: '27.1-jre' 5 | testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.1' 6 | } 7 | 8 | test { 9 | useJUnitPlatform() 10 | onlyIf { 11 | project.hasProperty('runExamples') 12 | } 13 | } 14 | 15 | publish { 16 | onlyIf { false } 17 | } 18 | -------------------------------------------------------------------------------- /examples/src/test/java/com/zendesk/jazon/junit/ExampleTest.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.zendesk.jazon.junit.JazonJunitAdapter.assertThat; 6 | 7 | class ExampleTest { 8 | 9 | /** 10 | * This is a passing test example. 11 | */ 12 | @Test 13 | void testRegex() { 14 | // given 15 | String actualJson = "{" + 16 | " \"first\": \"blue\"," + 17 | " \"second\": \"black\"," + 18 | " \"third\": \"red\"" + 19 | "}"; 20 | 21 | // then 22 | assertThat(actualJson).matches( 23 | new JazonMap() 24 | .with("first", (String s) -> s.matches("bl.*")) 25 | .with("second", s -> ((String) s).matches("bl.*")) 26 | .with("third", "red") 27 | ); 28 | } 29 | 30 | /** 31 | * This is a failing test example. 32 | */ 33 | @Test 34 | void testRegexTypeMismatch() { 35 | // given 36 | String actualJson = "{" + 37 | " \"first\": 55" + 38 | "}"; 39 | 40 | // then 41 | assertThat(actualJson).matches( 42 | new JazonMap() 43 | .with("first", (String s) -> s.matches("bl.*")) 44 | ); 45 | } 46 | 47 | /** 48 | * This is a failing test example. 49 | */ 50 | @Test 51 | void testPredicatedWithDeeplyNestedException() { 52 | // given 53 | String actualJson = "{" + 54 | " \"first\": 55" + 55 | "}"; 56 | 57 | // then 58 | assertThat(actualJson).matches( 59 | new JazonMap() 60 | .with("first", this::complexOperation) 61 | ); 62 | } 63 | 64 | private boolean complexOperation(Integer number) { 65 | return failingOperation(number + 10); 66 | } 67 | 68 | private boolean failingOperation(int number) { 69 | throw new RuntimeException("an intentional exception"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/src/test/java/com/zendesk/jazon/junit/ExamplesWithGuavaTest.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.function.Predicate; 9 | 10 | import static com.zendesk.jazon.junit.JazonJunitAdapter.assertThat; 11 | import static java.util.Arrays.asList; 12 | 13 | class ExamplesWithGuavaTest { 14 | 15 | /** 16 | * This is a failing test example. 17 | */ 18 | @Test 19 | void simpleTest() { 20 | // given 21 | String actualJson = "{\"value\": 123}"; 22 | Map expectedJsonAsMap = ImmutableMap.builder() 23 | .put("value", 50) 24 | .build(); 25 | 26 | // then 27 | assertThat(actualJson).matches(expectedJsonAsMap); 28 | } 29 | 30 | /** 31 | * This is a failing test example. 32 | */ 33 | @Test 34 | void testWithNestedArray() { 35 | // given 36 | String actualJson = "{" + 37 | "\"value\": 50," + 38 | "\"tags\": [\"blue\", \"black\", \"red\"]" + 39 | "}"; 40 | 41 | // then 42 | assertThat(actualJson).matches( 43 | deal(50, asList("blue", "pink", "red")) 44 | ); 45 | } 46 | 47 | /** 48 | * This is a failing test example. 49 | */ 50 | @Test 51 | void testWithRootArray() { 52 | // given 53 | String actualJson = "[\"blue\", \"black\", \"red\"]"; 54 | 55 | // then 56 | assertThat(actualJson).matches(asList("blue", "pink", "red")); 57 | } 58 | 59 | /** 60 | * This is a passing test example. 61 | */ 62 | @Test 63 | void testRegex() { 64 | // given 65 | String actualJson = "[\"blue\", \"black\", \"red\"]"; 66 | 67 | // then 68 | assertThat(actualJson).matches( 69 | asList( 70 | regex("bl.*"), 71 | regex("bl.*"), 72 | regex("r.*") 73 | ) 74 | ); 75 | } 76 | 77 | private Predicate regex(String reg) { 78 | return s -> s.matches(reg); 79 | } 80 | 81 | private static Map deal(int value, List tags) { 82 | return ImmutableMap.builder() 83 | .put("value", value) 84 | .put("tags", tags) 85 | .build(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/src/test/java/com/zendesk/jazon/junit/ReadmeExamplesTest.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.HashSet; 6 | import java.util.Objects; 7 | import java.util.Set; 8 | import java.util.function.Predicate; 9 | 10 | import static com.zendesk.jazon.junit.JazonJunitAdapter.assertThat; 11 | import static java.util.Arrays.asList; 12 | 13 | class ReadmeExamplesTest { 14 | private static final Predicate ANY_ID = (id) -> id >= 0; 15 | private static final Predicate ANY_ISO_DATETIME = 16 | datetime -> datetime.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); 17 | 18 | @Test 19 | void simpleAssertionPasses() { 20 | // when 21 | String response = "{\"firstname\": \"Steve\", \"lastname\": \"Jobs\"}"; 22 | 23 | // then 24 | assertThat(response).matches( 25 | new JazonMap() 26 | .with("firstname", "Steve") 27 | .with("lastname", "Jobs") 28 | ); 29 | } 30 | 31 | @Test 32 | void unorderedArrayAssertionPasses() { 33 | // when 34 | String response = "{" + 35 | " \"id\": 95478,\n" + 36 | " \"name\": \"Coca Cola\",\n" + 37 | " \"tags\": [\"sprite\", \"pepsi\", \"7up\", \"fanta\", \"dr pepper\"]\n" + 38 | "}"; 39 | 40 | // then 41 | assertThat(response).matches( 42 | new JazonMap() 43 | .with("id", 95478) 44 | .with("name", "Coca Cola") 45 | .with("tags", set("pepsi", "dr pepper", "sprite", "fanta", "7up")) 46 | ); 47 | } 48 | 49 | @Test 50 | void customAssertions() { 51 | // when 52 | String response = "{\n" + 53 | " \"id\": 95478,\n" + 54 | " \"name\": \"Coca Cola\",\n" + 55 | " \"value\": \"133.30\",\n" + 56 | " \"updated_at\": \"1990-06-19T12:19:10Z\"\n" + 57 | "}"; 58 | 59 | // then 60 | assertThat(response).matches( 61 | new JazonMap() 62 | .with("id", (Integer id) -> id >= 0) 63 | .with("name", "Coca Cola") 64 | .with("value", regex("\\d+\\.\\d\\d")) 65 | .with("updated_at", Objects::nonNull) 66 | ); 67 | } 68 | 69 | @Test 70 | void utilsExtraction() { 71 | // when 72 | String response = "{\n" + 73 | " \"id\": 95478,\n" + 74 | " \"name\": \"Coca Cola\",\n" + 75 | " \"value\": \"133.30\",\n" + 76 | " \"updated_at\": \"1990-06-19T12:19:10Z\"\n" + 77 | "}"; 78 | 79 | // then 80 | assertThat(response).matches( 81 | new JazonMap() 82 | .with("id", ANY_ID) 83 | .with("name", "Coca Cola") 84 | .with("value", "133.30") 85 | .with("updated_at", ANY_ISO_DATETIME) 86 | ); 87 | } 88 | 89 | @Test 90 | void utilsExtractionToDomainObjects() { 91 | // when 92 | String response = "{\n" + 93 | " \"id\": 95478,\n" + 94 | " \"name\": \"Coca Cola\",\n" + 95 | " \"value\": \"10.00\",\n" + 96 | " \"updated_at\": \"1990-06-19T12:19:10Z\"\n" + 97 | "}"; 98 | 99 | // then 100 | assertThat(response).matches(deal("Coca Cola", "10.00")); 101 | assertThat(response).matches( 102 | asList( 103 | deal("Coca Cola", "10.00"), 104 | deal("Pepsi", "9.00"), 105 | deal("Fanta", "10.00"), 106 | deal("Sprite", "10.00"), 107 | deal("Dr Pepper", "12.00") 108 | ) 109 | ); 110 | } 111 | 112 | private JazonMap deal(String name, String value) { 113 | return new JazonMap() 114 | .with("id", ANY_ID) 115 | .with("name", name) 116 | .with("value", value) 117 | .with("updated_at", ANY_ISO_DATETIME); 118 | } 119 | 120 | private Predicate regex(String regex) { 121 | return val -> val.matches(regex); 122 | } 123 | 124 | private Set set(Object... elements) { 125 | HashSet result = new HashSet<>(elements.length); 126 | result.addAll(asList(elements)); 127 | return result; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version = 0.4.1 2 | -------------------------------------------------------------------------------- /gradle/publishing.gradle: -------------------------------------------------------------------------------- 1 | publishing { 2 | publications { 3 | maven(MavenPublication) { 4 | from components.java 5 | artifact sourceJar 6 | artifact javadocJar 7 | 8 | pom { 9 | name = project.ext.name 10 | packaging = 'jar' 11 | description = project.ext.description 12 | url = 'https://github.com/zendesk/jazon' 13 | 14 | scm { 15 | connection = 'scm:git:git://github.com/zendesk/jazon.git' 16 | developerConnection = 'scm:git:ssh://github.com:zendesk/jazon.git' 17 | url = 'http://github.com/zendesk/jazon/tree/master' 18 | } 19 | 20 | licenses { 21 | license { 22 | name = 'The Apache License, Version 2.0' 23 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 24 | } 25 | } 26 | 27 | developers { 28 | developer { 29 | id = 'pawel' 30 | name = 'Paweł Mikołajczyk' 31 | email = 'pmikolajczyk@zendesk.com' 32 | organization = 'Zendesk' 33 | organizationUrl = 'https://zendesk.com' 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendesk/jazon/68d922d288ea6697e3de2bc65abca36449092efa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /jazon-core/build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | name = 'Jazon' 3 | description = 'A library for test assertions on JSON payloads.' 4 | } 5 | 6 | apply from: '../gradle/publishing.gradle' 7 | 8 | dependencies { 9 | compile group: 'com.google.code.gson', name: 'gson', version: '2.10.1' 10 | 11 | compileOnly 'org.projectlombok:lombok:1.18.12' 12 | annotationProcessor 'org.projectlombok:lombok:1.18.12' 13 | 14 | testCompile group: 'org.spockframework', name: 'spock-core', version: '1.2-groovy-2.4' 15 | } 16 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/MatchResult.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon; 2 | 3 | import com.zendesk.jazon.mismatch.MismatchWithPath; 4 | 5 | import java.util.Optional; 6 | import java.util.function.Supplier; 7 | 8 | import static java.util.Optional.empty; 9 | import static java.util.Optional.of; 10 | 11 | public class MatchResult { 12 | private final Optional mismatch; 13 | 14 | public static MatchResult success() { 15 | return new MatchResult(empty()); 16 | } 17 | 18 | public static MatchResult failure(MismatchWithPath mismatch) { 19 | return new MatchResult(of(mismatch)); 20 | } 21 | 22 | private MatchResult(Optional mismatch) { 23 | this.mismatch = mismatch; 24 | } 25 | 26 | public boolean ok() { 27 | return !mismatch.isPresent(); 28 | } 29 | 30 | public String message() { 31 | return mismatch 32 | .map(MismatchWithPath::message) 33 | .orElseThrow(cannotGetMessageException()); 34 | } 35 | 36 | public MismatchWithPath mismatch() { 37 | return mismatch 38 | .orElseThrow(cannotGetMismatchException()); 39 | } 40 | 41 | private Supplier cannotGetMessageException() { 42 | return () -> new IllegalStateException("MatchResult is OK. There is no Mismatch. You cannot get the message."); 43 | } 44 | 45 | private Supplier cannotGetMismatchException() { 46 | return () -> new IllegalStateException("MatchResult is OK. There is no Mismatch."); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/Matcher.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon; 2 | 3 | import com.zendesk.jazon.actual.Actual; 4 | import com.zendesk.jazon.actual.factory.ActualFactory; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | import com.zendesk.jazon.expectation.translator.TranslatorFacade; 7 | 8 | public class Matcher { 9 | private final TranslatorFacade translator; 10 | private final ActualFactory actualFactory; 11 | private JsonExpectation expectation; 12 | private Actual actual; 13 | 14 | Matcher(TranslatorFacade translator, ActualFactory actualFactory) { 15 | this.translator = translator; 16 | this.actualFactory = actualFactory; 17 | } 18 | 19 | public MatchResult match() { 20 | return actual.accept(expectation, "$"); 21 | } 22 | 23 | public Matcher expected(Object expectation) { 24 | this.expectation = translator.expectation(expectation); 25 | return this; 26 | } 27 | 28 | public Matcher actual(String actualJsonString) { 29 | this.actual = actualFactory.actual(actualJsonString); 30 | return this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/MatcherFactory.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon; 2 | 3 | import com.zendesk.jazon.actual.factory.ActualFactory; 4 | import com.zendesk.jazon.expectation.translator.TranslatorFacade; 5 | 6 | public class MatcherFactory { 7 | private final TranslatorFacade translator; 8 | private final ActualFactory actualFactory; 9 | 10 | public MatcherFactory(TranslatorFacade translator, ActualFactory actualFactory) { 11 | this.translator = translator; 12 | this.actualFactory = actualFactory; 13 | } 14 | 15 | public Matcher matcher() { 16 | return new Matcher(translator, actualFactory); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/Actual.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | 6 | public interface Actual { 7 | MatchResult accept(JsonExpectation expectation, String path); 8 | } 9 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/ActualJsonArray.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 11 | import static java.util.Collections.unmodifiableList; 12 | import static java.util.stream.Collectors.toList; 13 | 14 | @EqualsAndHashCode 15 | public class ActualJsonArray implements Actual { 16 | private final List list; 17 | 18 | public ActualJsonArray(List list) { 19 | this.list = checkNotNull(list); 20 | } 21 | 22 | public List list() { 23 | return unmodifiableList(list); 24 | } 25 | 26 | @Override 27 | public MatchResult accept(JsonExpectation expectation, String path) { 28 | return expectation.match(this, path); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return "[" + String.join(", ", strings()) + "]"; 34 | } 35 | 36 | private List strings() { 37 | return list.stream() 38 | .map(Objects::toString) 39 | .collect(toList()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/ActualJsonBoolean.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | import lombok.EqualsAndHashCode; 6 | 7 | @EqualsAndHashCode 8 | public class ActualJsonBoolean implements Actual { 9 | private final boolean value; 10 | 11 | public ActualJsonBoolean(boolean value) { 12 | this.value = value; 13 | } 14 | 15 | public boolean value() { 16 | return value; 17 | } 18 | 19 | @Override 20 | public MatchResult accept(JsonExpectation expectation, String path) { 21 | return expectation.match(this, path); 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return value ? "true" : "false"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/ActualJsonNull.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | 6 | public enum ActualJsonNull implements Actual { 7 | INSTANCE; 8 | 9 | @Override 10 | public MatchResult accept(JsonExpectation expectation, String path) { 11 | return expectation.match(this, path); 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return "null"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/ActualJsonNumber.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 10 | 11 | @EqualsAndHashCode 12 | public class ActualJsonNumber implements Actual { 13 | private final Number number; 14 | 15 | public ActualJsonNumber(Number number) { 16 | checkPreconditions(number); 17 | this.number = sanitized(number); 18 | } 19 | 20 | public Number number() { 21 | return number; 22 | } 23 | 24 | @Override 25 | public MatchResult accept(JsonExpectation expectation, String path) { 26 | return expectation.match(this, path); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return number.toString(); 32 | } 33 | 34 | private static void checkPreconditions(Number number) { 35 | checkNotNull(number); 36 | checkSupportedType(number); 37 | } 38 | 39 | private static void checkSupportedType(Number number) { 40 | if (isOfSupportedType(number)) { 41 | return; 42 | } 43 | throw new IllegalArgumentException( 44 | String.format( 45 | "Given Number must be either Integer, Long, BigDecimal, Float or Double. Found: %s (%s)", 46 | number, 47 | number.getClass() 48 | ) 49 | ); 50 | } 51 | 52 | private static boolean isOfSupportedType(Number number) { 53 | return number instanceof Integer || 54 | number instanceof Long || 55 | number instanceof BigDecimal || 56 | number instanceof Float || 57 | number instanceof Double; 58 | } 59 | 60 | private Number sanitized(Number number) { 61 | if (number instanceof Long 62 | && number.longValue() <= Integer.MAX_VALUE 63 | && number.longValue() >= Integer.MIN_VALUE) { 64 | return number.intValue(); 65 | } 66 | return number; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/ActualJsonObject.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import java.util.Map; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | import java.util.stream.Collectors; 11 | 12 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 13 | import static java.util.Collections.unmodifiableMap; 14 | 15 | @EqualsAndHashCode 16 | public class ActualJsonObject implements Actual { 17 | private final Map map; 18 | 19 | public ActualJsonObject(Map map) { 20 | this.map = checkNotNull(map); 21 | } 22 | 23 | public Optional actualField(String fieldName) { 24 | return Optional.ofNullable(map.get(fieldName)); 25 | } 26 | 27 | public Map map() { 28 | return unmodifiableMap(map); 29 | } 30 | 31 | public Set keys() { 32 | return map.keySet(); 33 | } 34 | 35 | @Override 36 | public MatchResult accept(JsonExpectation expectation, String path) { 37 | return expectation.match(this, path); 38 | } 39 | 40 | public int size() { 41 | return map.size(); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | if (map.isEmpty()) { 47 | return "{}"; 48 | } 49 | return "{" + fields() + "}"; 50 | } 51 | 52 | private String fields() { 53 | return map.entrySet().stream() 54 | .map(e -> String.format("\"%s\": %s", e.getKey(), e.getValue())) 55 | .collect(Collectors.joining(", ")); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/ActualJsonString.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 8 | 9 | @EqualsAndHashCode 10 | public class ActualJsonString implements Actual { 11 | private final String string; 12 | 13 | public ActualJsonString(String string) { 14 | this.string = checkNotNull(string); 15 | } 16 | 17 | public String string() { 18 | return string; 19 | } 20 | 21 | @Override 22 | public MatchResult accept(JsonExpectation expectation, String path) { 23 | return expectation.match(this, path); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return String.format("\"%s\"", string); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/factory/ActualFactory.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual.factory; 2 | 3 | import com.zendesk.jazon.actual.Actual; 4 | 5 | public interface ActualFactory { 6 | Actual actual(T input); 7 | } 8 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/actual/factory/GsonActualFactory.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual.factory; 2 | 3 | import com.google.gson.*; 4 | import com.zendesk.jazon.actual.Actual; 5 | import com.zendesk.jazon.actual.ActualJsonArray; 6 | import com.zendesk.jazon.actual.ActualJsonBoolean; 7 | import com.zendesk.jazon.actual.ActualJsonNull; 8 | import com.zendesk.jazon.actual.ActualJsonNumber; 9 | import com.zendesk.jazon.actual.ActualJsonObject; 10 | import com.zendesk.jazon.actual.ActualJsonString; 11 | import com.zendesk.jazon.actual.factory.ActualFactory; 12 | 13 | import java.math.BigDecimal; 14 | import java.util.LinkedHashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.stream.Stream; 18 | import java.util.stream.StreamSupport; 19 | 20 | import static java.util.stream.Collectors.toList; 21 | import static java.util.stream.Collectors.toMap; 22 | 23 | public class GsonActualFactory implements ActualFactory { 24 | private static final JsonParser JSON_PARSER = new JsonParser(); 25 | 26 | @Override 27 | public Actual actual(String input) { 28 | return fromJsonElement(JSON_PARSER.parse(input)); 29 | } 30 | 31 | private Actual fromJsonElement(JsonElement jsonElement) { 32 | if (jsonElement.isJsonObject()) { 33 | return actualJsonObject((JsonObject) jsonElement); 34 | } else if (jsonElement.isJsonArray()) { 35 | JsonArray jsonArray = (JsonArray) jsonElement; 36 | return actualJsonArray(jsonArray); 37 | } else if (jsonElement.isJsonNull()) { 38 | return ActualJsonNull.INSTANCE; 39 | } else if (jsonElement.isJsonPrimitive()) { 40 | return actualJsonPrimitive((JsonPrimitive) jsonElement); 41 | } 42 | throw new IllegalStateException("Invalid JsonElement - not Object, not Array, not Null, not Primitive"); 43 | } 44 | 45 | private ActualJsonObject actualJsonObject(JsonObject jsonObject) { 46 | Map map = jsonObject.entrySet() 47 | .stream() 48 | .collect( 49 | toMap( 50 | Map.Entry::getKey, 51 | e -> this.fromJsonElement(e.getValue()), 52 | (a, b) -> a, 53 | LinkedHashMap::new 54 | ) 55 | ); 56 | return new ActualJsonObject(map); 57 | } 58 | 59 | private ActualJsonArray actualJsonArray(JsonArray jsonArray) { 60 | List elements = stream(jsonArray) 61 | .map(this::fromJsonElement) 62 | .collect(toList()); 63 | return new ActualJsonArray(elements); 64 | 65 | } 66 | 67 | private Actual actualJsonPrimitive(JsonPrimitive jsonPrimitive) { 68 | if (jsonPrimitive.isBoolean()) { 69 | return new ActualJsonBoolean(jsonPrimitive.getAsBoolean()); 70 | } else if (jsonPrimitive.isNumber()) { 71 | return new ActualJsonNumber(intOrLongOrBigDecimal(jsonPrimitive)); 72 | } else if (jsonPrimitive.isString()) { 73 | return new ActualJsonString(jsonPrimitive.getAsString()); 74 | } 75 | throw new IllegalStateException("Invalid JsonPrimitive - not Boolean, not Number, not String"); 76 | } 77 | 78 | private Number intOrLongOrBigDecimal(JsonPrimitive jsonPrimitive) { 79 | String numberAsString = jsonPrimitive.getAsString(); 80 | if (isInteger(numberAsString)) { 81 | long numberAsLong = Long.parseLong(numberAsString); 82 | if (numberAsLong <= Integer.MAX_VALUE && numberAsLong >= Integer.MIN_VALUE) { 83 | return (int) numberAsLong; 84 | } 85 | return numberAsLong; 86 | } 87 | return new BigDecimal(numberAsString); 88 | } 89 | 90 | private boolean isInteger(String numberAsString) { 91 | return !numberAsString.contains(".") && 92 | !numberAsString.contains("e") && 93 | !numberAsString.contains("E"); 94 | } 95 | 96 | private Stream stream(JsonArray jsonArray) { 97 | return StreamSupport.stream(jsonArray.spliterator(), false); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/Expectations.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation; 2 | 3 | import com.zendesk.jazon.expectation.impl.AnyNumberOf; 4 | 5 | public class Expectations { 6 | 7 | public static AnyNumberOf anyNumberOf(Object element) { 8 | return new AnyNumberOf(element); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/JsonExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.*; 5 | 6 | public interface JsonExpectation { 7 | MatchResult match(ActualJsonNumber actualNumber, String path); 8 | MatchResult match(ActualJsonObject actualObject, String path); 9 | MatchResult match(ActualJsonString actualString, String path); 10 | MatchResult match(ActualJsonNull actualNull, String path); 11 | MatchResult match(ActualJsonArray actualArray, String path); 12 | MatchResult match(ActualJsonBoolean actualBoolean, String path); 13 | } 14 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/AnyNumberOf.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | public class AnyNumberOf { 8 | @Getter 9 | private final Object elementExpectation; 10 | } 11 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/AnyNumberOfExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.Actual; 5 | import com.zendesk.jazon.actual.ActualJsonArray; 6 | import com.zendesk.jazon.actual.ActualJsonBoolean; 7 | import com.zendesk.jazon.actual.ActualJsonNull; 8 | import com.zendesk.jazon.actual.ActualJsonNumber; 9 | import com.zendesk.jazon.actual.ActualJsonObject; 10 | import com.zendesk.jazon.actual.ActualJsonString; 11 | import com.zendesk.jazon.expectation.JsonExpectation; 12 | import com.zendesk.jazon.mismatch.MismatchWithPath; 13 | import com.zendesk.jazon.mismatch.impl.NullMismatch; 14 | import com.zendesk.jazon.mismatch.impl.TypeMismatch; 15 | import lombok.EqualsAndHashCode; 16 | import lombok.RequiredArgsConstructor; 17 | 18 | import java.util.ListIterator; 19 | 20 | import static com.zendesk.jazon.MatchResult.failure; 21 | import static com.zendesk.jazon.MatchResult.success; 22 | 23 | /** 24 | * Previously known as {@code ArrayEachElementExpectation} 25 | */ 26 | @RequiredArgsConstructor 27 | @EqualsAndHashCode 28 | public class AnyNumberOfExpectation implements JsonExpectation { 29 | private final JsonExpectation expectationForEachElement; 30 | 31 | @Override 32 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 33 | return failure(typeMismatch(ActualJsonNumber.class, path)); 34 | } 35 | 36 | @Override 37 | public MatchResult match(ActualJsonObject actualObject, String path) { 38 | return failure(typeMismatch(ActualJsonObject.class, path)); 39 | } 40 | 41 | @Override 42 | public MatchResult match(ActualJsonString actualString, String path) { 43 | return failure(typeMismatch(ActualJsonString.class, path)); 44 | } 45 | 46 | @Override 47 | public MatchResult match(ActualJsonNull actualNull, String path) { 48 | return failure( 49 | new NullMismatch<>(this) 50 | .at(path) 51 | ); 52 | } 53 | 54 | @Override 55 | public MatchResult match(ActualJsonArray actualArray, String path) { 56 | ListIterator actualValues = actualArray.list().listIterator(); 57 | while (actualValues.hasNext()) { 58 | Actual actualValue = actualValues.next(); 59 | MatchResult matchResult = actualValue.accept(expectationForEachElement, path + "." + actualValues.previousIndex()); 60 | if (!matchResult.ok()) { 61 | return matchResult; 62 | } 63 | } 64 | return success(); 65 | } 66 | 67 | @Override 68 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 69 | return failure(typeMismatch(ActualJsonBoolean.class, path)); 70 | } 71 | 72 | private MismatchWithPath typeMismatch(Class actualType, String path) { 73 | return new TypeMismatch(ActualJsonArray.class, actualType) 74 | .at(path); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/NullExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.*; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | import com.zendesk.jazon.mismatch.MismatchWithPath; 7 | import com.zendesk.jazon.mismatch.impl.NotNullMismatch; 8 | import lombok.EqualsAndHashCode; 9 | 10 | import static com.zendesk.jazon.MatchResult.failure; 11 | import static com.zendesk.jazon.MatchResult.success; 12 | 13 | @EqualsAndHashCode 14 | public class NullExpectation implements JsonExpectation { 15 | @Override 16 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 17 | return failure(notNullMismatch(actualNumber, path)); 18 | } 19 | 20 | @Override 21 | public MatchResult match(ActualJsonObject actualObject, String path) { 22 | return failure(notNullMismatch(actualObject, path)); 23 | } 24 | 25 | @Override 26 | public MatchResult match(ActualJsonString actualString, String path) { 27 | return failure(notNullMismatch(actualString, path)); 28 | } 29 | 30 | @Override 31 | public MatchResult match(ActualJsonNull actualNull, String path) { 32 | return success(); 33 | } 34 | 35 | @Override 36 | public MatchResult match(ActualJsonArray actualArray, String path) { 37 | return failure(notNullMismatch(actualArray, path)); 38 | } 39 | 40 | @Override 41 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 42 | return failure(notNullMismatch(actualBoolean, path)); 43 | } 44 | 45 | private MismatchWithPath notNullMismatch(Actual actual, String path) { 46 | return new NotNullMismatch(actual) 47 | .at(path); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "null"; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/ObjectExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.*; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | import com.zendesk.jazon.mismatch.*; 7 | import com.zendesk.jazon.mismatch.impl.NoFieldMismatch; 8 | import com.zendesk.jazon.mismatch.impl.NullMismatch; 9 | import com.zendesk.jazon.mismatch.impl.TypeMismatch; 10 | import com.zendesk.jazon.mismatch.impl.UnexpectedFieldMismatch; 11 | import lombok.AllArgsConstructor; 12 | import lombok.EqualsAndHashCode; 13 | 14 | import java.util.HashSet; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 21 | import static com.zendesk.jazon.MatchResult.failure; 22 | 23 | @EqualsAndHashCode 24 | public class ObjectExpectation implements JsonExpectation { 25 | private final Map expectationMap; 26 | 27 | public ObjectExpectation(Map expectationMap) { 28 | this.expectationMap = checkNotNull(expectationMap); 29 | } 30 | 31 | @Override 32 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 33 | return failure(typeMismatch(ActualJsonNumber.class, path)); 34 | } 35 | 36 | @Override 37 | public MatchResult match(ActualJsonObject actualObject, String path) { 38 | Optional mismatchFromExpectedFields = mismatchFromExpectedFields(actualObject, path); 39 | Optional mismatchFromUnexpected = mismatchFromUnexpected(actualObject, path); 40 | return firstOf(mismatchFromExpectedFields, mismatchFromUnexpected) 41 | .map(MatchResult::failure) 42 | .orElseGet(MatchResult::success); 43 | } 44 | 45 | @Override 46 | public MatchResult match(ActualJsonString actualString, String path) { 47 | return failure(typeMismatch(ActualJsonString.class, path)); 48 | } 49 | 50 | @Override 51 | public MatchResult match(ActualJsonNull actualNull, String path) { 52 | return failure( 53 | new NullMismatch<>(this) 54 | .at(path) 55 | ); 56 | } 57 | 58 | @Override 59 | public MatchResult match(ActualJsonArray actualArray, String path) { 60 | return failure(typeMismatch(ActualJsonArray.class, path)); 61 | } 62 | 63 | @Override 64 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 65 | return failure(typeMismatch(ActualJsonBoolean.class, path)); 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return partialJsonObject(2); 71 | } 72 | 73 | private String partialJsonObject(int firstFieldsCount) { 74 | if (expectationMap.isEmpty()) { 75 | return "{}"; 76 | } 77 | String firstFields = expectationMap.entrySet().stream() 78 | .limit(firstFieldsCount) 79 | .map(e -> String.format("\"%s\": %s", e.getKey(), e.getValue())) 80 | .collect(Collectors.joining(", ")); 81 | String suffix = firstFieldsCount < expectationMap.size() ? ", ...}" : "}"; 82 | return "{" + firstFields + suffix; 83 | } 84 | 85 | private Optional mismatchFromExpectedFields(ActualJsonObject actualObject, String path) { 86 | return new MismatchFactory(actualObject, path) 87 | .mismatchFromExpectedFields(); 88 | } 89 | 90 | private Optional mismatchFromUnexpected(ActualJsonObject actualObject, String path) { 91 | return new MismatchFactory(actualObject, path) 92 | .mismatchFromUnexpected(); 93 | } 94 | 95 | private static Optional firstOf(Optional first, Optional second) { 96 | if (first.isPresent()) { 97 | return first; 98 | } 99 | return second; 100 | } 101 | 102 | private MismatchWithPath typeMismatch(Class actualType, String path) { 103 | return new TypeMismatch(ActualJsonObject.class, actualType) 104 | .at(path); 105 | } 106 | 107 | @AllArgsConstructor 108 | private class MismatchFactory { 109 | private final ActualJsonObject actualObject; 110 | private final String path; 111 | 112 | Optional mismatchFromExpectedFields() { 113 | return expectationMap.entrySet() 114 | .stream() 115 | .map(e -> matchResult(e.getKey(), e.getValue())) 116 | .filter(matchResult -> !matchResult.ok()) 117 | .map(MatchResult::mismatch) 118 | .findFirst(); 119 | } 120 | 121 | private Optional mismatchFromUnexpected() { 122 | Set unexpectedFields = setsDifference(actualObject.keys(), expectationMap.keySet()); 123 | return unexpectedFields.stream() 124 | .map(fieldName -> 125 | new UnexpectedFieldMismatch(fieldName) 126 | .at(path) 127 | ) 128 | .findFirst(); 129 | } 130 | 131 | private MatchResult matchResult(String fieldName, JsonExpectation expectation) { 132 | return actualObject.actualField(fieldName) 133 | .map(actual -> actual.accept(expectation, path + "." + fieldName)) 134 | .orElseGet(() -> 135 | failure( 136 | new NoFieldMismatch(fieldName, expectation) 137 | .at(path) 138 | ) 139 | ); 140 | } 141 | 142 | private Set setsDifference(Set first, Set second) { 143 | HashSet result = new HashSet<>(); 144 | for (String memberOfFirst : first) { 145 | if (!second.contains(memberOfFirst)) { 146 | result.add(memberOfFirst); 147 | } 148 | } 149 | return result; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/OrderedArrayExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.*; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | import com.zendesk.jazon.mismatch.*; 7 | import com.zendesk.jazon.mismatch.impl.ArrayLackingElementsMismatch; 8 | import com.zendesk.jazon.mismatch.impl.ArrayUnexpectedElementsMismatch; 9 | import com.zendesk.jazon.mismatch.impl.NullMismatch; 10 | import com.zendesk.jazon.mismatch.impl.TypeMismatch; 11 | import lombok.EqualsAndHashCode; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.Iterator; 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 20 | import static com.zendesk.jazon.MatchResult.failure; 21 | import static com.zendesk.jazon.MatchResult.success; 22 | 23 | @EqualsAndHashCode 24 | public class OrderedArrayExpectation implements JsonExpectation { 25 | private final List expectationList; 26 | 27 | public OrderedArrayExpectation(List expectationList) { 28 | this.expectationList = checkNotNull(expectationList); 29 | } 30 | 31 | @Override 32 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 33 | return failure(typeMismatch(ActualJsonNumber.class, path)); 34 | } 35 | 36 | @Override 37 | public MatchResult match(ActualJsonObject actualObject, String path) { 38 | return failure(typeMismatch(ActualJsonObject.class, path)); 39 | } 40 | 41 | @Override 42 | public MatchResult match(ActualJsonString actualString, String path) { 43 | return failure(typeMismatch(ActualJsonString.class, path)); 44 | } 45 | 46 | @Override 47 | public MatchResult match(ActualJsonNull actualNull, String path) { 48 | return failure( 49 | new NullMismatch<>(this) 50 | .at(path) 51 | ); 52 | } 53 | 54 | @Override 55 | public MatchResult match(ActualJsonArray actualArray, String path) { 56 | int index = 0; 57 | Iterator expectationIterator = expectationList.iterator(); 58 | Iterator actualIterator = actualArray.list().iterator(); 59 | 60 | while (expectationIterator.hasNext() && actualIterator.hasNext()) { 61 | JsonExpectation expectation = expectationIterator.next(); 62 | Actual actual = actualIterator.next(); 63 | MatchResult matchResult = actual.accept(expectation, path + "." + index); 64 | if (!matchResult.ok()) { 65 | return matchResult; 66 | } 67 | index += 1; 68 | } 69 | 70 | if (expectationIterator.hasNext()) { 71 | List lackingElements = remainingItems(expectationIterator); 72 | return failure( 73 | new ArrayLackingElementsMismatch(lackingElements) 74 | .at(path) 75 | ); 76 | } 77 | 78 | if (actualIterator.hasNext()) { 79 | List unexpectedElements = remainingItems(actualIterator); 80 | return failure( 81 | new ArrayUnexpectedElementsMismatch(unexpectedElements) 82 | .at(path) 83 | ); 84 | } 85 | 86 | return success(); 87 | } 88 | 89 | @Override 90 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 91 | return failure(typeMismatch(ActualJsonBoolean.class, path)); 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "[" + String.join(", ", strings(expectationList)) + "]"; 97 | } 98 | 99 | private List remainingItems(Iterator iterator) { 100 | ArrayList result = new ArrayList<>(); 101 | iterator.forEachRemaining(result::add); 102 | return result; 103 | } 104 | 105 | private MismatchWithPath typeMismatch(Class actualType, String path) { 106 | return new TypeMismatch(ActualJsonArray.class, actualType) 107 | .at(path); 108 | } 109 | 110 | private Collection strings(Collection objects) { 111 | return objects.stream() 112 | .map(Object::toString) 113 | .collect(Collectors.toList()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/PredicateExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.Actual; 5 | import com.zendesk.jazon.actual.ActualJsonArray; 6 | import com.zendesk.jazon.actual.ActualJsonBoolean; 7 | import com.zendesk.jazon.actual.ActualJsonNull; 8 | import com.zendesk.jazon.actual.ActualJsonNumber; 9 | import com.zendesk.jazon.actual.ActualJsonObject; 10 | import com.zendesk.jazon.actual.ActualJsonString; 11 | import com.zendesk.jazon.expectation.JsonExpectation; 12 | import com.zendesk.jazon.mismatch.impl.PredicateExecutionFailedMismatch; 13 | import com.zendesk.jazon.mismatch.impl.PredicateMismatch; 14 | import lombok.EqualsAndHashCode; 15 | import lombok.ToString; 16 | 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.function.Predicate; 21 | 22 | import static com.zendesk.jazon.MatchResult.failure; 23 | import static com.zendesk.jazon.MatchResult.success; 24 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 25 | import static java.util.stream.Collectors.toList; 26 | 27 | @ToString 28 | @EqualsAndHashCode 29 | public class PredicateExpectation implements JsonExpectation { 30 | private final Predicate predicate; 31 | 32 | public PredicateExpectation(Predicate predicate) { 33 | this.predicate = checkNotNull(predicate); 34 | } 35 | 36 | @Override 37 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 38 | return matchUnwrapped(actualNumber, path); 39 | } 40 | 41 | @Override 42 | public MatchResult match(ActualJsonObject actualObject, String path) { 43 | return matchUnwrapped(actualObject, path); 44 | } 45 | 46 | @Override 47 | public MatchResult match(ActualJsonString actualString, String path) { 48 | return matchUnwrapped(actualString, path); 49 | } 50 | 51 | @Override 52 | public MatchResult match(ActualJsonNull actualNull, String path) { 53 | return matchUnwrapped(actualNull, path); 54 | } 55 | 56 | @Override 57 | public MatchResult match(ActualJsonArray actualArray, String path) { 58 | return matchUnwrapped(actualArray, path); 59 | } 60 | 61 | @Override 62 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 63 | return matchUnwrapped(actualBoolean, path); 64 | } 65 | 66 | private MatchResult matchUnwrapped(Actual actual, String path) { 67 | Predicate objectPredicate = (Predicate) predicate; 68 | try { 69 | return objectPredicate.test(unwrap(actual)) 70 | ? success() 71 | : failure(PredicateMismatch.INSTANCE.at(path)); 72 | } catch (Exception e) { 73 | return failure(new PredicateExecutionFailedMismatch(e).at(path)); 74 | } 75 | } 76 | 77 | private Map unwrapObject(ActualJsonObject actualObject) { 78 | Map resultMap = new HashMap<>(actualObject.size()); 79 | for (Map.Entry entry : actualObject.map().entrySet()) { 80 | resultMap.put(entry.getKey(), unwrap(entry.getValue())); 81 | } 82 | return resultMap; 83 | } 84 | 85 | private List unwrapArray(ActualJsonArray actualJsonArray) { 86 | return actualJsonArray.list().stream() 87 | .map(this::unwrap) 88 | .collect(toList()); 89 | } 90 | 91 | private Object unwrap(Actual actual) { 92 | if (actual instanceof ActualJsonString) { 93 | return ((ActualJsonString) actual).string(); 94 | } else if (actual instanceof ActualJsonNumber) { 95 | return ((ActualJsonNumber) actual).number(); 96 | } else if (actual instanceof ActualJsonBoolean) { 97 | return ((ActualJsonBoolean) actual).value(); 98 | } else if (actual instanceof ActualJsonNull) { 99 | return null; 100 | } else if (actual instanceof ActualJsonObject) { 101 | return unwrapObject((ActualJsonObject) actual); 102 | } else if (actual instanceof ActualJsonArray) { 103 | return unwrapArray((ActualJsonArray) actual); 104 | } 105 | throw new IllegalArgumentException("Not a valid Actual object: " + actual); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/PrimitiveValueExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.*; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | import com.zendesk.jazon.mismatch.MismatchWithPath; 7 | import com.zendesk.jazon.mismatch.impl.NullMismatch; 8 | import com.zendesk.jazon.mismatch.impl.PrimitiveValueMismatch; 9 | import com.zendesk.jazon.mismatch.impl.TypeMismatch; 10 | import lombok.EqualsAndHashCode; 11 | 12 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 13 | import static com.zendesk.jazon.MatchResult.failure; 14 | import static com.zendesk.jazon.MatchResult.success; 15 | 16 | @EqualsAndHashCode 17 | public class PrimitiveValueExpectation implements JsonExpectation { 18 | private final T expectedValue; 19 | 20 | public PrimitiveValueExpectation(T expectedValue) { 21 | this.expectedValue = checkNotNull(expectedValue); 22 | } 23 | 24 | @Override 25 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 26 | return matchPrimitive(actualNumber, path); 27 | } 28 | 29 | @Override 30 | public MatchResult match(ActualJsonObject actualObject, String path) { 31 | return failure(typeMismatch(ActualJsonObject.class, path)); 32 | } 33 | 34 | @Override 35 | public MatchResult match(ActualJsonString actualString, String path) { 36 | return matchPrimitive(actualString, path); 37 | } 38 | 39 | @Override 40 | public MatchResult match(ActualJsonNull actualNull, String path) { 41 | return failure( 42 | new NullMismatch<>(this) 43 | .at(path) 44 | ); 45 | } 46 | 47 | @Override 48 | public MatchResult match(ActualJsonArray actualArray, String path) { 49 | return failure(typeMismatch(ActualJsonArray.class, path)); 50 | } 51 | 52 | @Override 53 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 54 | return matchPrimitive(actualBoolean, path); 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return expectedValue.toString(); 60 | } 61 | 62 | private MatchResult matchPrimitive(ActualType actualValue, String path) { 63 | if (actualValue.getClass() != expectedType()) { 64 | return failure(typeMismatch(actualValue.getClass(), path)); 65 | } 66 | if (expectedValue.equals(actualValue)) { 67 | return success(); 68 | } 69 | return failure( 70 | new PrimitiveValueMismatch<>(expectedValue, actualValue) 71 | .at(path) 72 | ); 73 | } 74 | 75 | private MismatchWithPath typeMismatch(Class actualType, String path) { 76 | return new TypeMismatch(expectedType(), actualType) 77 | .at(path); 78 | } 79 | 80 | private Class expectedType() { 81 | return expectedValue.getClass(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/impl/UnorderedArrayExpectation.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.impl; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.actual.*; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | import com.zendesk.jazon.mismatch.*; 7 | import com.zendesk.jazon.mismatch.impl.ArrayLackingElementsMismatch; 8 | import com.zendesk.jazon.mismatch.impl.ArrayUnexpectedElementsMismatch; 9 | import com.zendesk.jazon.mismatch.impl.NullMismatch; 10 | import com.zendesk.jazon.mismatch.impl.TypeMismatch; 11 | import lombok.EqualsAndHashCode; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collection; 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | import java.util.stream.Collectors; 18 | 19 | import static com.zendesk.jazon.MatchResult.failure; 20 | import static com.zendesk.jazon.MatchResult.success; 21 | import static java.util.Arrays.asList; 22 | 23 | /** 24 | * FIXME: 25 | * There are 2 problems with this implementation: 26 | * 1. `match(ActualJsonArray)` method has time complexity of O(n^2) which is high cost for large sets. 27 | * 2. Due to naive implementation of `match(ActualJsonArray)` method, expectation types that are not 28 | * exact-equality-matches are not supported (currently the only such example is UnorderedArrayExpectation but 29 | * soon expected CustomPredicateExpectation similarly will not be supported in UnorderedArrayExpectation). 30 | * {@code SUPPORTED_EXPECTATION_TYPES} defines which types of expectation classes are supported. 31 | */ 32 | @EqualsAndHashCode 33 | public class UnorderedArrayExpectation implements JsonExpectation { 34 | private static final Set> SUPPORTED_EXPECTATION_TYPES = new HashSet<>(asList( 35 | PrimitiveValueExpectation.class, 36 | ObjectExpectation.class, 37 | OrderedArrayExpectation.class 38 | )); 39 | private final Set expectationSet; 40 | 41 | public UnorderedArrayExpectation(Set expectationSet) { 42 | expectationSet.forEach(this::verifyExpectationSupported); 43 | this.expectationSet = expectationSet; 44 | } 45 | 46 | @Override 47 | public MatchResult match(ActualJsonNumber actualNumber, String path) { 48 | return failure(typeMismatch(ActualJsonNumber.class, path)); 49 | } 50 | 51 | @Override 52 | public MatchResult match(ActualJsonObject actualObject, String path) { 53 | return failure(typeMismatch(ActualJsonObject.class, path)); 54 | } 55 | 56 | @Override 57 | public MatchResult match(ActualJsonString actualString, String path) { 58 | return failure(typeMismatch(ActualJsonString.class, path)); 59 | } 60 | 61 | @Override 62 | public MatchResult match(ActualJsonNull actualNull, String path) { 63 | return failure( 64 | new NullMismatch<>(this) 65 | .at(path) 66 | ); 67 | } 68 | 69 | @Override 70 | public MatchResult match(ActualJsonArray actualArray, String path) { 71 | Set stillExpected = new HashSet<>(expectationSet); 72 | ArrayList actualList = new ArrayList<>(actualArray.list()); 73 | 74 | for (JsonExpectation expectation : expectationSet) { 75 | for (int actualIndex = 0; actualIndex < actualList.size(); actualIndex++) { 76 | Actual actual = actualList.get(actualIndex); 77 | MatchResult result = actual.accept(expectation, path + ".?"); 78 | if (result.ok()) { 79 | actualList.remove(actual); 80 | stillExpected.remove(expectation); 81 | break; 82 | } 83 | } 84 | } 85 | 86 | if (!stillExpected.isEmpty()) { 87 | return failure( 88 | new ArrayLackingElementsMismatch(stillExpected) 89 | .at(path) 90 | ); 91 | } 92 | if (!actualList.isEmpty()) { 93 | return failure( 94 | new ArrayUnexpectedElementsMismatch(actualList) 95 | .at(path) 96 | ); 97 | } 98 | return success(); 99 | } 100 | 101 | @Override 102 | public MatchResult match(ActualJsonBoolean actualBoolean, String path) { 103 | return failure(typeMismatch(ActualJsonBoolean.class, path)); 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | return "[" + String.join(", ", strings(expectationSet)) + "] (unordered)"; 109 | } 110 | 111 | private MismatchWithPath typeMismatch(Class actualType, String path) { 112 | return new TypeMismatch(ActualJsonArray.class, actualType) 113 | .at(path); 114 | } 115 | 116 | private void verifyExpectationSupported(JsonExpectation expectation) { 117 | boolean isSupported = SUPPORTED_EXPECTATION_TYPES.contains(expectation.getClass()); 118 | if (!isSupported) { 119 | throw new IllegalStateException( 120 | String.format( 121 | "%s is not supported in %s", 122 | expectation.getClass(), 123 | UnorderedArrayExpectation.class.toString() 124 | ) 125 | ); 126 | } 127 | } 128 | 129 | private Collection strings(Collection objects) { 130 | return objects.stream() 131 | .map(Object::toString) 132 | .collect(Collectors.toList()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/translator/DefaultTranslators.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.actual.ActualJsonBoolean; 4 | import com.zendesk.jazon.actual.ActualJsonNumber; 5 | import com.zendesk.jazon.actual.ActualJsonString; 6 | import com.zendesk.jazon.expectation.JsonExpectation; 7 | import com.zendesk.jazon.expectation.impl.ObjectExpectation; 8 | import com.zendesk.jazon.expectation.impl.OrderedArrayExpectation; 9 | import com.zendesk.jazon.expectation.impl.PredicateExpectation; 10 | import com.zendesk.jazon.expectation.impl.PrimitiveValueExpectation; 11 | import com.zendesk.jazon.expectation.impl.UnorderedArrayExpectation; 12 | 13 | import java.util.LinkedHashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Set; 17 | import java.util.function.Predicate; 18 | import java.util.stream.Stream; 19 | 20 | import static java.util.Arrays.asList; 21 | import static java.util.stream.Collectors.toList; 22 | import static java.util.stream.Collectors.toMap; 23 | import static java.util.stream.Collectors.toSet; 24 | 25 | public class DefaultTranslators { 26 | public static List> translators() { 27 | return asList( 28 | new TranslatorMapping<>(Map.class, new MapTranslator()), 29 | new TranslatorMapping<>(List.class, new ListTranslator()), 30 | new TranslatorMapping<>(Set.class, new SetTranslator()), 31 | new TranslatorMapping<>(Predicate.class, new PredicateTranslator()), 32 | new TranslatorMapping<>(Number.class, new NumberTranslator()), 33 | new TranslatorMapping<>(String.class, new StringTranslator()), 34 | new TranslatorMapping<>(Boolean.class, new BooleanTranslator()) 35 | ); 36 | } 37 | 38 | @SuppressWarnings({"rawtypes", "unchecked"}) 39 | private static class MapTranslator implements Translator { 40 | @Override 41 | public JsonExpectation jsonExpectation(Map object, TranslatorFacade translator) { 42 | Map objectsMap = (Map) object; 43 | LinkedHashMap expectationsMap = objectsMap.entrySet() 44 | .stream() 45 | .collect( 46 | toMap( 47 | e -> e.getKey().toString(), 48 | e -> translator.expectation(e.getValue()), 49 | (a, b) -> a, 50 | () -> new LinkedHashMap<>(objectsMap.size()) 51 | ) 52 | ); 53 | return new ObjectExpectation(expectationsMap); 54 | 55 | } 56 | } 57 | 58 | @SuppressWarnings({"rawtypes", "unchecked"}) 59 | private static class ListTranslator implements Translator { 60 | @Override 61 | public JsonExpectation jsonExpectation(List objectsList, TranslatorFacade translator) { 62 | Stream stream = objectsList.stream() 63 | .map(translator::expectation); 64 | List expectations = stream.collect(toList()); 65 | return new OrderedArrayExpectation(expectations); 66 | } 67 | } 68 | 69 | @SuppressWarnings({"rawtypes", "unchecked"}) 70 | private static class SetTranslator implements Translator { 71 | @Override 72 | public JsonExpectation jsonExpectation(Set objectsSet, TranslatorFacade translator) { 73 | Stream stream = objectsSet.stream() 74 | .map(translator::expectation); 75 | Set expectations = stream.collect(toSet()); 76 | return new UnorderedArrayExpectation(expectations); 77 | } 78 | } 79 | 80 | @SuppressWarnings("rawtypes") 81 | private static class PredicateTranslator implements Translator { 82 | @Override 83 | public JsonExpectation jsonExpectation(Predicate predicate, TranslatorFacade translator) { 84 | return new PredicateExpectation(predicate); 85 | } 86 | } 87 | 88 | private static class NumberTranslator implements Translator { 89 | @Override 90 | public JsonExpectation jsonExpectation(Number number, TranslatorFacade translator) { 91 | return new PrimitiveValueExpectation<>(new ActualJsonNumber(number)); 92 | } 93 | } 94 | 95 | private static class StringTranslator implements Translator { 96 | @Override 97 | public JsonExpectation jsonExpectation(String string, TranslatorFacade translator) { 98 | return new PrimitiveValueExpectation<>(new ActualJsonString(string)); 99 | } 100 | } 101 | 102 | private static class BooleanTranslator implements Translator { 103 | @Override 104 | public JsonExpectation jsonExpectation(Boolean bool, TranslatorFacade translator) { 105 | return new PrimitiveValueExpectation<>(new ActualJsonBoolean(bool)); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/translator/JazonTypesTranslators.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.expectation.impl.AnyNumberOf; 4 | import com.zendesk.jazon.expectation.impl.AnyNumberOfExpectation; 5 | import com.zendesk.jazon.expectation.JsonExpectation; 6 | 7 | import java.util.List; 8 | 9 | import static java.util.Collections.singletonList; 10 | 11 | public class JazonTypesTranslators { 12 | public static List> translators() { 13 | return singletonList( 14 | new TranslatorMapping<>(AnyNumberOf.class, new AnyNumberOfTranslator()) 15 | ); 16 | } 17 | 18 | private static class AnyNumberOfTranslator implements Translator { 19 | @Override 20 | public JsonExpectation jsonExpectation(AnyNumberOf anyNumberOf, TranslatorFacade translator) { 21 | Object repeatedObject = anyNumberOf.getElementExpectation(); 22 | JsonExpectation repeatedExpectation = translator.expectation(repeatedObject); 23 | return new AnyNumberOfExpectation(repeatedExpectation); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/translator/Translator.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | 5 | public interface Translator { 6 | JsonExpectation jsonExpectation(T object, TranslatorFacade translator); 7 | } 8 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/translator/TranslatorFacade.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | import com.zendesk.jazon.expectation.impl.NullExpectation; 5 | import lombok.AllArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @AllArgsConstructor 10 | public class TranslatorFacade { 11 | private final List> translatorMappings; 12 | 13 | public JsonExpectation expectation(Object object) { 14 | if (object == null) { 15 | return new NullExpectation(); 16 | } 17 | for (TranslatorMapping translatorMapping : translatorMappings) { 18 | if (translatorMapping.supports(object)) { 19 | return translatorMapping.jsonExpectation(object, this); 20 | } 21 | } 22 | throw new IllegalArgumentException(String.format("Could not map this object to expectation: %s", object)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/expectation/translator/TranslatorMapping.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | import lombok.AllArgsConstructor; 5 | 6 | @AllArgsConstructor 7 | public class TranslatorMapping { 8 | private final Class klass; 9 | private final Translator translator; 10 | 11 | boolean supports(Object object) { 12 | return klass.isInstance(object); 13 | } 14 | 15 | JsonExpectation jsonExpectation(Object object, TranslatorFacade translator) { 16 | return this.translator.jsonExpectation(cast(object), translator); 17 | } 18 | 19 | private T cast(Object object) { 20 | if (!supports(object)) { 21 | throw new IllegalArgumentException("Given object is not supported: " + object); 22 | } 23 | return klass.cast(object); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/Mismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch; 2 | 3 | public interface Mismatch { 4 | String message(); 5 | } 6 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/MismatchWithPath.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.ToString; 5 | 6 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 7 | 8 | @ToString 9 | @EqualsAndHashCode 10 | public class MismatchWithPath { 11 | private final Mismatch internalMismatch; 12 | private final String path; 13 | 14 | public MismatchWithPath(Mismatch internalMismatch, String path) { 15 | this.internalMismatch = checkNotNull(internalMismatch); 16 | this.path = checkNotNull(path); 17 | } 18 | 19 | public Mismatch expectationMismatch() { 20 | return internalMismatch; 21 | } 22 | 23 | public String path() { 24 | return path; 25 | } 26 | 27 | public String message() { 28 | return "Mismatch at path: " + path + "\n" + internalMismatch.message(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/MismatchWithPathFactory.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch; 2 | 3 | public interface MismatchWithPathFactory { 4 | default MismatchWithPath at(String path) { 5 | return new MismatchWithPath((Mismatch) this, path); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/ArrayLackingElementsMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import java.util.Collection; 10 | 11 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 12 | 13 | @ToString 14 | @EqualsAndHashCode 15 | public class ArrayLackingElementsMismatch implements Mismatch, MismatchWithPathFactory { 16 | private final Collection lackingElements; 17 | 18 | public ArrayLackingElementsMismatch(Collection lackingElements) { 19 | this.lackingElements = checkNotNull(lackingElements); 20 | } 21 | 22 | @Override 23 | public String message() { 24 | return String.format("Array lacks the items: %s", lackingElements); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/ArrayUnexpectedElementsMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.actual.Actual; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import java.util.List; 10 | 11 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 12 | 13 | @ToString 14 | @EqualsAndHashCode 15 | public class ArrayUnexpectedElementsMismatch implements Mismatch, MismatchWithPathFactory { 16 | private final List unexpectedElements; 17 | 18 | public ArrayUnexpectedElementsMismatch(List unexpectedElements) { 19 | this.unexpectedElements = checkNotNull(unexpectedElements); 20 | } 21 | 22 | @Override 23 | public String message() { 24 | return String.format("Array contains unexpected items: %s", unexpectedElements); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/NoFieldMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 10 | 11 | @EqualsAndHashCode 12 | @ToString 13 | public class NoFieldMismatch implements Mismatch, MismatchWithPathFactory { 14 | private final String fieldName; 15 | private final JsonExpectation expectation; 16 | 17 | public NoFieldMismatch(String fieldName, JsonExpectation expectation) { 18 | this.fieldName = checkNotNull(fieldName); 19 | this.expectation = checkNotNull(expectation); 20 | } 21 | 22 | @Override 23 | public String message() { 24 | return String.format("Could not find expected field (\"%s\": %s)", fieldName, expectation); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/NotNullMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.actual.Actual; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 10 | 11 | @ToString 12 | @EqualsAndHashCode 13 | public class NotNullMismatch implements Mismatch, MismatchWithPathFactory { 14 | private final Actual actual; 15 | 16 | public NotNullMismatch(Actual actual) { 17 | this.actual = checkNotNull(actual); 18 | } 19 | 20 | @Override 21 | public String message() { 22 | return String.format("Expected null. Found: %s", actual); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/NullMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 10 | 11 | @ToString 12 | @EqualsAndHashCode 13 | public class NullMismatch implements Mismatch, MismatchWithPathFactory { 14 | private final T expectedValue; 15 | 16 | public NullMismatch(T expectedValue) { 17 | this.expectedValue = checkNotNull(expectedValue); 18 | } 19 | 20 | @Override 21 | public String message() { 22 | return String.format("Found null. Expected: %s", expectedValue); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/PredicateExecutionFailedMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.mismatch.Mismatch; 4 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | import java.io.PrintWriter; 9 | import java.io.StringWriter; 10 | 11 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 12 | 13 | @EqualsAndHashCode 14 | @ToString 15 | public class PredicateExecutionFailedMismatch implements Mismatch, MismatchWithPathFactory { 16 | private final Throwable cause; 17 | 18 | public PredicateExecutionFailedMismatch(Throwable cause) { 19 | this.cause = checkNotNull(cause); 20 | } 21 | 22 | @Override 23 | public String message() { 24 | StringWriter stringWriter = new StringWriter(); 25 | 26 | stringWriter.append("Exception occurred on predicate evaluation: \n\n"); 27 | cause.printStackTrace(new PrintWriter(stringWriter)); 28 | 29 | return stringWriter.toString(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/PredicateMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.mismatch.Mismatch; 4 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 5 | import lombok.ToString; 6 | 7 | @ToString 8 | public enum PredicateMismatch implements Mismatch, MismatchWithPathFactory { 9 | INSTANCE; 10 | 11 | @Override 12 | public String message() { 13 | return "Custom predicate does not match the value."; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/PrimitiveValueMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.actual.Actual; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 10 | 11 | @ToString 12 | @EqualsAndHashCode 13 | public class PrimitiveValueMismatch implements Mismatch, MismatchWithPathFactory { 14 | private final T expected; 15 | private final T actual; 16 | 17 | public PrimitiveValueMismatch(T expected, T actual) { 18 | this.expected = checkNotNull(expected); 19 | this.actual = checkNotNull(actual); 20 | } 21 | 22 | @Override 23 | public String message() { 24 | return String.format("Expected: %s\nActual: %s", expected, actual); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/TypeMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.actual.*; 4 | import com.zendesk.jazon.mismatch.Mismatch; 5 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.ToString; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 13 | import static java.util.Collections.unmodifiableMap; 14 | import static java.util.Optional.ofNullable; 15 | 16 | @ToString 17 | @EqualsAndHashCode 18 | public class TypeMismatch implements Mismatch, MismatchWithPathFactory { 19 | private static final Map, String> JSON_TYPES = jsonTypes(); 20 | 21 | private final Class expectedType; 22 | 23 | private final Class actualType; 24 | public TypeMismatch(Class expectedType, Class actualType) { 25 | this.expectedType = checkNotNull(expectedType); 26 | this.actualType = checkNotNull(actualType); 27 | } 28 | 29 | @Override 30 | public String message() { 31 | return String.format("Expected type: %s\nActual type: %s", string(expectedType), string(actualType)); 32 | } 33 | 34 | private String string(Class jsonType) { 35 | return ofNullable(JSON_TYPES.get(jsonType)) 36 | .orElseThrow(() -> new IllegalArgumentException("Invalid JSON type")); 37 | } 38 | 39 | private static Map, String> jsonTypes() { 40 | HashMap, String> jsonTypes = new HashMap<>(); 41 | jsonTypes.put(ActualJsonObject.class, "Object"); 42 | jsonTypes.put(ActualJsonArray.class, "Array"); 43 | jsonTypes.put(ActualJsonString.class, "String"); 44 | jsonTypes.put(ActualJsonNumber.class, "Number"); 45 | jsonTypes.put(ActualJsonBoolean.class, "Boolean"); 46 | jsonTypes.put(ActualJsonNull.class, "Null"); 47 | return unmodifiableMap(jsonTypes); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/mismatch/impl/UnexpectedFieldMismatch.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch.impl; 2 | 3 | import com.zendesk.jazon.mismatch.Mismatch; 4 | import com.zendesk.jazon.mismatch.MismatchWithPathFactory; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.ToString; 7 | 8 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 9 | 10 | @ToString 11 | @EqualsAndHashCode 12 | public class UnexpectedFieldMismatch implements Mismatch, MismatchWithPathFactory { 13 | private final String fieldName; 14 | 15 | public UnexpectedFieldMismatch(String fieldName) { 16 | this.fieldName = checkNotNull(fieldName); 17 | } 18 | 19 | @Override 20 | public String message() { 21 | return String.format("Unexpected field \"%s\" in object.", fieldName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /jazon-core/src/main/java/com/zendesk/jazon/util/Preconditions.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.util; 2 | 3 | import lombok.NoArgsConstructor; 4 | 5 | import static lombok.AccessLevel.PRIVATE; 6 | 7 | @NoArgsConstructor(access = PRIVATE) 8 | public final class Preconditions { 9 | 10 | public static T checkNotNull(T object) { 11 | if (object == null) { 12 | throw new NullPointerException(); 13 | } 14 | return object; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jazon-core/src/test/groovy/com/zendesk/jazon/MatcherSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon 2 | 3 | 4 | import com.zendesk.jazon.actual.ActualJsonArray 5 | import com.zendesk.jazon.actual.ActualJsonBoolean 6 | import com.zendesk.jazon.actual.ActualJsonNull 7 | import com.zendesk.jazon.actual.ActualJsonNumber 8 | import com.zendesk.jazon.actual.ActualJsonObject 9 | import com.zendesk.jazon.actual.ActualJsonString 10 | import com.zendesk.jazon.actual.factory.GsonActualFactory 11 | import com.zendesk.jazon.expectation.translator.DefaultTranslators 12 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators 13 | import com.zendesk.jazon.expectation.JsonExpectation 14 | import com.zendesk.jazon.expectation.impl.NullExpectation 15 | import com.zendesk.jazon.expectation.impl.PrimitiveValueExpectation 16 | import com.zendesk.jazon.expectation.translator.TranslatorFacade 17 | import com.zendesk.jazon.mismatch.impl.ArrayLackingElementsMismatch 18 | import com.zendesk.jazon.mismatch.impl.ArrayUnexpectedElementsMismatch 19 | import com.zendesk.jazon.mismatch.impl.NoFieldMismatch 20 | import com.zendesk.jazon.mismatch.impl.NotNullMismatch 21 | import com.zendesk.jazon.mismatch.impl.NullMismatch 22 | import com.zendesk.jazon.mismatch.impl.PredicateMismatch 23 | import com.zendesk.jazon.mismatch.impl.PrimitiveValueMismatch 24 | import com.zendesk.jazon.mismatch.impl.TypeMismatch 25 | import com.zendesk.jazon.mismatch.impl.UnexpectedFieldMismatch 26 | import spock.lang.Specification 27 | import spock.lang.Unroll 28 | 29 | import java.util.function.Predicate 30 | 31 | import static com.zendesk.jazon.expectation.Expectations.anyNumberOf 32 | import static groovy.json.JsonOutput.toJson 33 | 34 | class MatcherSpec extends Specification { 35 | private static TestActualFactory testActualFactory = new TestActualFactory() 36 | private static MatcherFactory matcherFactory = new MatcherFactory( 37 | new TranslatorFacade(DefaultTranslators.translators() + JazonTypesTranslators.translators()), 38 | new GsonActualFactory() 39 | ) 40 | 41 | @Unroll 42 | def "primitive value mismatch (expected: #expected, actual: #actual)"() { 43 | when: 44 | def result = match([a: expected], [a: actual]) 45 | 46 | then: 47 | !result.ok() 48 | result.mismatch().expectationMismatch() == primitiveValueMismatch(expected, actual) 49 | result.mismatch().path() == '$.a' 50 | 51 | where: 52 | expected | actual 53 | 123 | 10 54 | 123 | new BigDecimal("11.05") 55 | 123 | 12345l 56 | 130.1f | 10 57 | 130.1f | new BigDecimal("11.05") 58 | 130.1f | 12345l 59 | 1500.13d | 10 60 | 1500.13d | new BigDecimal("11.05") 61 | 1500.13d | 12345l 62 | new BigDecimal("11.05") | 10 63 | new BigDecimal("11.05") | new BigDecimal("11.11") 64 | new BigDecimal("11.05") | 12345l 65 | 12345l | 10 66 | 12345l | new BigDecimal("11.05") 67 | 12345l | 1234567l 68 | 'green' | 'red' 69 | true | false 70 | false | true 71 | } 72 | 73 | @Unroll 74 | def "primitive value mismatch for floating Actuals (expected: #expected, actual: #actual)"() { 75 | when: 76 | def result = match([a: expected], [a: actualFloating]) 77 | 78 | then: 79 | !result.ok() 80 | result.mismatch().expectationMismatch() == primitiveValueMismatch(expected, actualDecimal) 81 | result.mismatch().path() == '$.a' 82 | 83 | where: 84 | expected | actualFloating | actualDecimal 85 | 123 | 130.1f | new BigDecimal('130.1') 86 | 123 | 1500.13d | new BigDecimal('1500.13') 87 | 130.1f | 133.3f | new BigDecimal('133.3') 88 | 130.1f | 1500.13d | new BigDecimal('1500.13') 89 | 1500.13d | 130.1f | new BigDecimal('130.1') 90 | 1500.13d | 1555.55d | new BigDecimal('1555.55') 91 | new BigDecimal("11.05") | 130.1f | new BigDecimal('130.1') 92 | new BigDecimal("11.05") | 1500.13d | new BigDecimal('1500.13') 93 | 12345l | 130.1f | new BigDecimal('130.1') 94 | 12345l | 1500.13d | new BigDecimal('1500.13') 95 | } 96 | 97 | def "simple primitive type mismatch"() { 98 | when: 99 | def result = match(expected, actual) 100 | 101 | then: 102 | !result.ok() 103 | result.mismatch().expectationMismatch() == new TypeMismatch(mismatchExpectedType, mismatchActualType) 104 | result.mismatch().path() == '$.a' 105 | 106 | where: 107 | expected | actual | mismatchExpectedType | mismatchActualType 108 | [a: 123] | [a: 'red'] | ActualJsonNumber.class | ActualJsonString.class 109 | [a: 123] | [a: [bb: 10]] | ActualJsonNumber.class | ActualJsonObject.class 110 | [a: 123] | [a: true] | ActualJsonNumber.class | ActualJsonBoolean.class 111 | [a: 123] | [a: [1, 2]] | ActualJsonNumber.class | ActualJsonArray.class 112 | [a: 'ww'] | [a: 88] | ActualJsonString.class | ActualJsonNumber.class 113 | [a: 'ww'] | [a: [bb: 10]] | ActualJsonString.class | ActualJsonObject.class 114 | [a: 'ww'] | [a: true] | ActualJsonString.class | ActualJsonBoolean.class 115 | [a: 'ww'] | [a: [1, 2]] | ActualJsonString.class | ActualJsonArray.class 116 | [a: [bb: 10]] | [a: 88] | ActualJsonObject.class | ActualJsonNumber.class 117 | [a: [bb: 10]] | [a: 'red'] | ActualJsonObject.class | ActualJsonString.class 118 | [a: [bb: 10]] | [a: true] | ActualJsonObject.class | ActualJsonBoolean.class 119 | [a: [bb: 10]] | [a: [1, 2]] | ActualJsonObject.class | ActualJsonArray.class 120 | [a: true] | [a: 'red'] | ActualJsonBoolean.class | ActualJsonString.class 121 | [a: true] | [a: 88] | ActualJsonBoolean.class | ActualJsonNumber.class 122 | [a: true] | [a: [bb: 10]] | ActualJsonBoolean.class | ActualJsonObject.class 123 | [a: true] | [a: [1, 2]] | ActualJsonBoolean.class | ActualJsonArray.class 124 | [a: [1, 2]] | [a: 123] | ActualJsonArray.class | ActualJsonNumber.class 125 | [a: [1, 2]] | [a: 'red'] | ActualJsonArray.class | ActualJsonString.class 126 | [a: [1, 2]] | [a: 88] | ActualJsonArray.class | ActualJsonNumber.class 127 | [a: [1, 2]] | [a: [bb: 10]] | ActualJsonArray.class | ActualJsonObject.class 128 | [a: [1, 2] as Set] | [a: 123] | ActualJsonArray.class | ActualJsonNumber.class 129 | [a: [1, 2] as Set] | [a: 'red'] | ActualJsonArray.class | ActualJsonString.class 130 | [a: [1, 2] as Set] | [a: 88] | ActualJsonArray.class | ActualJsonNumber.class 131 | [a: [1, 2] as Set] | [a: [bb: 10]] | ActualJsonArray.class | ActualJsonObject.class 132 | } 133 | 134 | def "matches numbers in object even if they have different types"() { 135 | when: 136 | def result = match([a: expected], [a: actual]) 137 | 138 | then: 139 | result.ok() 140 | 141 | where: 142 | expected | actual 143 | 1 as int | 1 as long 144 | 1 as long | 1 as int 145 | } 146 | 147 | def "matches numbers in array even if they have different types"() { 148 | when: 149 | def result = match([a: [1, 2, expected]], [a: [1, 2, actual]]) 150 | 151 | then: 152 | result.ok() 153 | 154 | where: 155 | expected | actual 156 | 3 as int | 3 as long 157 | 3 as long | 3 as int 158 | } 159 | 160 | @Unroll 161 | def "finds null instead of primitive value: #expected"() { 162 | when: 163 | def result = match([a: expected], [a: null]) 164 | 165 | then: 166 | !result.ok() 167 | result.mismatch().expectationMismatch() == new NullMismatch(expectationInstance) 168 | result.mismatch().path() == '$.a' 169 | 170 | where: 171 | expected | expectationInstance 172 | 123 | primitive(123) 173 | 130.1f | primitive(130.1f) 174 | 1500.13d | primitive(1500.13d) 175 | new BigDecimal("11.05") | primitive(new BigDecimal("11.05")) 176 | 12345l | primitive(12345l) 177 | "sting" | primitive("sting") 178 | true | primitive(true) 179 | } 180 | 181 | @Unroll 182 | def "mismatch in object field (expected: #expectedFieldValue, actual: #actualFieldValue)"() { 183 | given: 184 | def expected = [ 185 | a: 103, 186 | b: expectedFieldValue 187 | ] 188 | def actual = [ 189 | a: 103, 190 | b: actualFieldValue, 191 | ] 192 | 193 | when: 194 | def result = match(expected, actual) 195 | 196 | then: 197 | !result.ok() 198 | result.mismatch().expectationMismatch() == foundMismatch 199 | result.mismatch().path() == mismatchPath 200 | 201 | where: 202 | expectedFieldValue | actualFieldValue || mismatchPath | foundMismatch 203 | 'vegetable' | 'meat' || '$.b' | primitiveValueMismatch('vegetable', 'meat') 204 | 'vegetable' | null || '$.b' | new NullMismatch<>(primitive('vegetable')) 205 | 'vegetable' | 150 || '$.b' | new TypeMismatch(ActualJsonString, ActualJsonNumber) 206 | 77 | 'rosemary' || '$.b' | new TypeMismatch(ActualJsonNumber, ActualJsonString) 207 | [] | 'car' || '$.b' | new TypeMismatch(ActualJsonArray, ActualJsonString) 208 | [20, 30] | [20, 77] || '$.b.1' | primitiveValueMismatch(30, 77) 209 | } 210 | 211 | def "catches lacking field in Object"() { 212 | given: 213 | def expected = [ 214 | a: 103, 215 | b: 'some value', 216 | ] 217 | 218 | when: 219 | def result = match(expected, actual) 220 | 221 | then: 222 | !result.ok() 223 | result.mismatch().expectationMismatch() == new NoFieldMismatch( 224 | 'b', 225 | new PrimitiveValueExpectation<>(new ActualJsonString('some value')) 226 | ) 227 | result.mismatch().path() == '$' 228 | 229 | where: 230 | actual << [ 231 | [a: 103], 232 | [a: 103, c: 'car'] 233 | ] 234 | } 235 | 236 | @Unroll 237 | def "catches unexpected field in Object: #unexpectedFieldValue"() { 238 | given: 239 | def expected = [ 240 | a: 103, 241 | c: 'Chicago', 242 | ] 243 | def actual = [ 244 | a: 103, 245 | b: unexpectedFieldValue, 246 | c: 'Chicago', 247 | ] 248 | 249 | when: 250 | def result = match(expected, actual) 251 | 252 | then: 253 | !result.ok() 254 | result.mismatch().expectationMismatch() == new UnexpectedFieldMismatch('b') 255 | result.mismatch().path() == '$' 256 | 257 | where: 258 | unexpectedFieldValue | unexpectedFieldType 259 | 'act of vandalism' | ActualJsonString 260 | 123 | ActualJsonNumber 261 | 1999l | ActualJsonNumber 262 | 20.14f | ActualJsonNumber 263 | 44.999d | ActualJsonNumber 264 | new BigDecimal("80.92") | ActualJsonNumber 265 | [a: 1, b: 'blue'] | ActualJsonObject 266 | null | ActualJsonNull 267 | [5, 4, 3] | ActualJsonArray 268 | true | ActualJsonBoolean 269 | false | ActualJsonBoolean 270 | } 271 | 272 | @Unroll 273 | def "object expectation - type mismatch for #actualType"() { 274 | given: 275 | def theObject = [ 276 | id : 1, 277 | name : "Leo", 278 | nationality: "Argentinian" 279 | ] 280 | def expected = [a: theObject] 281 | def actual = [a: actualValue] 282 | 283 | when: 284 | def result = match(expected, actual) 285 | 286 | then: 287 | !result.ok() 288 | result.mismatch().expectationMismatch() == new TypeMismatch(ActualJsonObject, actualType) 289 | result.mismatch().path() == '$.a' 290 | 291 | where: 292 | actualValue | actualType 293 | true | ActualJsonBoolean 294 | 130 | ActualJsonNumber 295 | 'orange' | ActualJsonString 296 | [1, 2, 3] | ActualJsonArray 297 | } 298 | 299 | @Unroll 300 | def "ordered list expectation - exact element mismatch"() { 301 | when: 302 | def result = match([a: expected], [a: actual]) 303 | 304 | then: 305 | !result.ok() 306 | result.mismatch().expectationMismatch() == elementMismatch 307 | result.mismatch().path() == '$.a.' + elementIndex 308 | 309 | where: 310 | expected | actual || elementIndex | elementMismatch 311 | [1, 2, 3] | [3, 2, 1] || 0 | primitiveValueMismatch(1, 3) 312 | [1, 2, 3] | [1, 7, 3] || 1 | primitiveValueMismatch(2, 7) 313 | [1, 2, true] | [1, 2, 3] || 2 | new TypeMismatch(ActualJsonBoolean, ActualJsonNumber) 314 | [1, 2, 3] | [1, 2, true] || 2 | new TypeMismatch(ActualJsonNumber, ActualJsonBoolean) 315 | [1, null, 3] | [1, 2, 3] || 1 | new NotNullMismatch(new ActualJsonNumber(2)) 316 | [1, 2, 3] | [1, null, 3] || 1 | new NullMismatch<>(primitive(2)) 317 | [1, 2, 3] | [1, 2, 4, 5] || 2 | primitiveValueMismatch(3, 4) 318 | } 319 | 320 | @Unroll 321 | def "ordered list expectation - lacking elements (expected: #expected, actual: #actual)"() { 322 | when: 323 | def result = match([a: expected], [a: actual]) 324 | 325 | then: 326 | !result.ok() 327 | result.mismatch().expectationMismatch() == new ArrayLackingElementsMismatch( 328 | lackingElements.collect(this.&expectation) 329 | ) 330 | result.mismatch().path() == '$.a' 331 | 332 | where: 333 | expected | actual || lackingElements 334 | [1, 2, 3] | [1, 2] || [3] 335 | [1, 2, 'lalala'] | [1, 2] || ['lalala'] 336 | [1, 2, 'lalala', 5, 6, 7] | [1, 2] || ['lalala', 5, 6, 7] 337 | [1, 2, null, 5, 6, 7] | [1, 2, null, 5] || [6, 7] 338 | [1, null] | [1] || [null] 339 | [9] | [] || [9] 340 | [null] | [] || [null] 341 | [null, 'car', 17] | [] || [null, 'car', 17] 342 | } 343 | 344 | def "ordered list expectation - unexpected elements"() { 345 | when: 346 | def result = match([a: expected], [a: actual]) 347 | 348 | then: 349 | !result.ok() 350 | result.mismatch().expectationMismatch() == new ArrayUnexpectedElementsMismatch( 351 | unexpectedElements.collect(testActualFactory.&actual) 352 | ) 353 | result.mismatch().path() == '$.a' 354 | 355 | where: 356 | expected | actual || unexpectedElements 357 | [1, 2] | [1, 2, 3] || [3] 358 | [] | [1, 2, 3] || [1, 2, 3] 359 | ['carpet'] | ['carpet', 'fur'] || ['fur'] 360 | [] | [null] || [null] 361 | [] | [null, null] || [null, null] 362 | [1, 2] | [1, 2, null] || [null] 363 | [true, 'bike'] | [true, 'bike', false] || [false] 364 | } 365 | 366 | @Unroll 367 | def "ordered list expectation - type mismatch for #actualType"() { 368 | given: 369 | def theArray = ['white bear', 'seal', 'penguin'] 370 | def expected = [a: theArray] 371 | def actual = [a: actualValue] 372 | 373 | when: 374 | def result = match(expected, actual) 375 | 376 | then: 377 | !result.ok() 378 | result.mismatch().expectationMismatch() == new TypeMismatch(ActualJsonArray, actualType) 379 | result.mismatch().path() == '$.a' 380 | 381 | where: 382 | actualValue | actualType 383 | true | ActualJsonBoolean 384 | 130 | ActualJsonNumber 385 | 'orange' | ActualJsonString 386 | [a: 44] | ActualJsonObject 387 | } 388 | 389 | def "unordered list expectation: success"() { 390 | when: 391 | def result = match([a: expected], [a: actual]) 392 | 393 | then: 394 | result.ok() 395 | 396 | where: 397 | expected | actual 398 | [1, 2, 3] as Set | [1, 3, 2] 399 | [1, 2, 3] as Set | [3, 1, 2] 400 | [1, 2, 'lalala'] as Set | ['lalala', 1, 2] 401 | [1, 2, 'lalala', 5, 6, 7] as Set | [6, 7, 2, 5, 'lalala', 1] 402 | } 403 | 404 | @Unroll 405 | def "unordered list expectation: lacking elements (actual: #actual)"() { 406 | when: 407 | def result = match([a: expected], [a: actual]) 408 | 409 | then: 410 | !result.ok() 411 | result.mismatch().expectationMismatch() == new ArrayLackingElementsMismatch( 412 | lackingElements.collect(this.&expectation) as Set 413 | ) 414 | result.mismatch().path() == '$.a' 415 | 416 | where: 417 | expected | actual || lackingElements 418 | [1, 2, 3] as Set | [1, 3] || [2] 419 | [1, 2, 3] as Set | [1, 3, 1] || [2] 420 | [1, 2, 3] as Set | [3, 1, 1, 88] || [2] 421 | [1, 2, 3] as Set | [3, 1, 3, 1] || [2] 422 | [1, 2, 'lalala'] as Set | ['lalala', 11, 2] || [1] 423 | [1, 2, 'lalala', 5, 6, 7] as Set | [6, 7, 2, 5, 'rob', 1] || ['lalala'] 424 | [3, 4, 2, 1] as Set | [11, 2, 3, 55] || [1, 4] 425 | [3, 4, 2, 1] as Set | [1] || [2, 3, 4] 426 | [3, 4, 2, 1] as Set | [2] || [1, 3, 4] 427 | [3, 4, 2, 1] as Set | [3] || [1, 2, 4] 428 | [3, 4, 2, 1] as Set | [4] || [1, 2, 3] 429 | } 430 | 431 | def "unordered list expectation: unexpected elements"() { 432 | when: 433 | def result = match([a: expected], [a: actual]) 434 | 435 | then: 436 | !result.ok() 437 | result.mismatch().expectationMismatch() == new ArrayUnexpectedElementsMismatch( 438 | unexpectedElements.collect(testActualFactory.&actual) 439 | ) 440 | result.mismatch().path() == '$.a' 441 | 442 | where: 443 | expected | actual || unexpectedElements 444 | [1, 2, 3] as Set | [1, 3, 2, 8] || [8] 445 | [1, 2, 3] as Set | [1, 3, 2, 'sushi'] || ['sushi'] 446 | [1, 2, 3] as Set | [1, 'sushi', 2, 3] || ['sushi'] 447 | [1, 2, 3] as Set | [1, 3, 2, null] || [null] 448 | ['what', 'is', 'love'] as Set | ['love', 'is', 10, 'what'] || [10] 449 | } 450 | 451 | @Unroll 452 | def "unordered list expectation - type mismatch for #actualType"() { 453 | given: 454 | def theSet = ['white bear', 'seal', 'penguin'] as Set 455 | def expected = [a: theSet] 456 | def actual = [a: actualValue] 457 | 458 | when: 459 | def result = match(expected, actual) 460 | 461 | then: 462 | !result.ok() 463 | result.mismatch().expectationMismatch() == new TypeMismatch(ActualJsonArray, actualType) 464 | result.mismatch().path() == '$.a' 465 | 466 | where: 467 | actualValue | actualType 468 | true | ActualJsonBoolean 469 | 130 | ActualJsonNumber 470 | 'orange' | ActualJsonString 471 | [a: 44] | ActualJsonObject 472 | } 473 | 474 | def "unordered list expectation: fails for unsupported expectation types"() { 475 | given: 476 | def unsupportedExpectation = [1, 2, 3] as Set 477 | def unorderedArrayExpectationWrapping = ['fish', 'chips', unsupportedExpectation] as Set 478 | 479 | when: 480 | match([a: unorderedArrayExpectationWrapping], [a: unorderedArrayExpectationWrapping]) 481 | 482 | then: 483 | thrown(IllegalStateException) 484 | } 485 | 486 | @Unroll 487 | def "array each element expectation: success"() { 488 | expect: 489 | match([a: anyNumberOf(expected)], [a: actual]).success() 490 | 491 | where: 492 | expected | actual 493 | '1' | [] 494 | '1' | ['1'] 495 | '1' | ['1', '1'] 496 | true | [true] 497 | 2 | [2] 498 | [b: true, c: 1] | [[[b: true, c: 1]]] 499 | [3, 4, 5] | [[3, 4, 5]] 500 | { it -> it > 5 } as Predicate | [6, 7, 8] 501 | } 502 | 503 | @Unroll 504 | def "array each element expectation - element mismatch"() { 505 | when: 506 | def result = match([a: anyNumberOf(expected)], [a: actual]) 507 | 508 | then: 509 | !result.ok() 510 | result.mismatch().expectationMismatch() == elementMismatch 511 | result.mismatch().path() == '$.a.' + path 512 | 513 | where: 514 | expected | actual || path | elementMismatch | _ 515 | 1 | [1, 3, 1] || '1' | primitiveValueMismatch(1, 3) | _ 516 | 1 | [1, 1, true] || '2' | new TypeMismatch(ActualJsonNumber, ActualJsonBoolean) | _ 517 | true | [true, 1, true] || '1' | new TypeMismatch(ActualJsonBoolean, ActualJsonNumber) | _ 518 | 1 | [1, null, 1] || '1' | new NullMismatch<>(expectation(1)) | _ 519 | [b: true, c: 1] | [[b: true, c: 2]] || '0.c' | primitiveValueMismatch(1, 2) | _ 520 | [3, 4, 5] | [[3, 4, false]] || '0.2' | new TypeMismatch(ActualJsonNumber, ActualJsonBoolean) | _ 521 | ({ it -> it > 3 } 522 | as Predicate) | [4, 5, 2] || '2' | PredicateMismatch.INSTANCE | _ 523 | } 524 | 525 | def "null expectation: fails for any present value"() { 526 | when: 527 | def result = match([a: null], [a: actual]) 528 | 529 | then: 530 | !result.ok() 531 | result.mismatch().expectationMismatch() == new NotNullMismatch(testActualFactory.actual(actual)) 532 | result.mismatch().path() == '$.a' 533 | 534 | where: 535 | actual << [ 536 | 'something', 537 | 10, 538 | new BigDecimal("11.05"), 539 | 12345l, 540 | [x: 123], 541 | [1, 2, 3], 542 | true, 543 | false 544 | ] 545 | } 546 | 547 | def "null expectation: fails for any present float/double"() { 548 | when: 549 | def result = match([a: null], [a: actualFloating]) 550 | 551 | then: 552 | !result.ok() 553 | result.mismatch().expectationMismatch() == new NotNullMismatch(testActualFactory.actual(actualDecimal)) 554 | result.mismatch().path() == '$.a' 555 | 556 | where: 557 | actualFloating | actualDecimal 558 | 130.1f | new BigDecimal('130.1') 559 | 1555.55d | new BigDecimal('1555.55') 560 | } 561 | 562 | def "null expectation: succeeds for null"() { 563 | when: 564 | def result = match([a: null], [a: null]) 565 | 566 | then: 567 | result.ok() 568 | } 569 | 570 | def "any expectation can be root expectation"() { 571 | when: 572 | def result = matcherFactory.matcher() 573 | .expected(expected) 574 | .actual(toJson(actual)) 575 | .match() 576 | 577 | then: 578 | !result.ok() 579 | result.mismatch().expectationMismatch() == mismatch 580 | result.mismatch().path() == path 581 | 582 | where: 583 | expected | actual || path | mismatch 584 | [1, 2, 3] | [1, 88, 3] || '$.1' | primitiveValueMismatch(2, 88) 585 | [1, 2, 3] as Set | [3, 1, 99] || '$' | new ArrayLackingElementsMismatch([expectation(2)] as Set) 586 | 'medicine' | 'drug' || '$' | primitiveValueMismatch('medicine', 'drug') 587 | 100 | 99 || '$' | primitiveValueMismatch(100, 99) 588 | true | false || '$' | primitiveValueMismatch(true, false) 589 | null | 'vegetables' || '$' | new NotNullMismatch(testActualFactory.actual('vegetables')) 590 | [a: 1] | [a: 9] || '$.a' | primitiveValueMismatch(1, 9) 591 | } 592 | 593 | def "Groovy's GString can be a key in expectation"() { 594 | given: 595 | String key = 'name' 596 | 597 | when: 598 | def result = match(["$key": 'Andreas'], [name: 'Andreas']) 599 | 600 | then: 601 | noExceptionThrown() 602 | result.ok() 603 | } 604 | 605 | private static MatchResult match(Map expected, Map actual) { 606 | matcherFactory.matcher() 607 | .expected(expected) 608 | .actual(toJson(actual)) 609 | .match() 610 | } 611 | 612 | private static PrimitiveValueMismatch primitiveValueMismatch(def expected, def actual) { 613 | return new PrimitiveValueMismatch(testActualFactory.actual(expected), testActualFactory.actual(actual)) 614 | } 615 | 616 | private static JsonExpectation expectation(Object object) { 617 | if (object == null) { 618 | return new NullExpectation() 619 | } 620 | return primitive(object) 621 | } 622 | 623 | private static PrimitiveValueExpectation primitive(Object object) { 624 | return new PrimitiveValueExpectation(testActualFactory.actual(object)) 625 | } 626 | } 627 | -------------------------------------------------------------------------------- /jazon-core/src/test/groovy/com/zendesk/jazon/MessagesSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon 2 | 3 | import com.zendesk.jazon.actual.factory.GsonActualFactory 4 | import com.zendesk.jazon.expectation.translator.DefaultTranslators 5 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators 6 | import com.zendesk.jazon.expectation.translator.TranslatorFacade 7 | import spock.lang.Specification 8 | import spock.lang.Unroll 9 | 10 | import static groovy.json.JsonOutput.toJson 11 | 12 | class MessagesSpec extends Specification { 13 | private static final MatcherFactory matcherFactory = new MatcherFactory( 14 | new TranslatorFacade(DefaultTranslators.translators() + JazonTypesTranslators.translators()), 15 | new GsonActualFactory() 16 | ) 17 | 18 | def "object expectation - primitive field value mismatch"() { 19 | given: 20 | def expected = [ 21 | a: expectedValue 22 | ] 23 | def actual = [ 24 | a: actualValue 25 | ] 26 | 27 | when: 28 | def result = match(expected, actual) 29 | 30 | then: 31 | result.message() == "Mismatch at path: \$.a\nExpected: $expectedInMessage\nActual: $actualInMessage" 32 | print result 33 | 34 | where: 35 | expectedValue | actualValue || expectedInMessage | actualInMessage 36 | 'lance' | 'vance' || '"lance"' | '"vance"' 37 | 120 | 50 || '120' | '50' 38 | 44.50 | 180.10 || '44.50' | '180.10' 39 | true | false || 'true' | 'false' 40 | } 41 | 42 | def "object expectation - unexpected field"() { 43 | given: 44 | def expected = [ 45 | a: 'lance' 46 | ] 47 | def actual = [ 48 | a: 'lance', 49 | b: 'helicopter' 50 | ] 51 | 52 | when: 53 | def result = match(expected, actual) 54 | 55 | then: 56 | result.message() == 'Mismatch at path: $\nUnexpected field "b" in object.' 57 | print result 58 | } 59 | 60 | @Unroll 61 | def "object expectation - no field (#expectedValue)"() { 62 | given: 63 | def expected = [ 64 | a: expectedValue 65 | ] 66 | def actual = [ 67 | b: 'strong wind' 68 | ] 69 | 70 | when: 71 | def result = match(expected, actual) 72 | 73 | then: 74 | result.message() == "Mismatch at path: \$\nCould not find expected field (\"a\": $expectedInMessage)" 75 | print result 76 | 77 | where: 78 | expectedValue | expectedInMessage 79 | 'lance' | '"lance"' 80 | 120 | '120' 81 | true | 'true' 82 | false | 'false' 83 | null | 'null' 84 | [1, 2, 3] | '[1, 2, 3]' 85 | ['milk', 'sugar', 'flour'] | '["milk", "sugar", "flour"]' 86 | [1, 2, 3] as Set | '[1, 2, 3] (unordered)' 87 | [name: 'Wayne', surname: 'Rooney'] | '{"name": "Wayne", "surname": "Rooney"}' 88 | [uno: 1, due: 2, tres: 3, quatro: 4] | '{"uno": 1, "due": 2, ...}' 89 | } 90 | 91 | def "object expectation: found null instead of #expectedValue"() { 92 | given: 93 | def expected = [ 94 | a: 'refrigerator' 95 | ] 96 | def actual = [ 97 | a: null 98 | ] 99 | 100 | when: 101 | def result = match(expected, actual) 102 | 103 | then: 104 | result.message() == 'Mismatch at path: \$.a\nFound null. Expected: "refrigerator"' 105 | print result 106 | } 107 | 108 | def "object expectation: found something instead of null"() { 109 | given: 110 | def expected = [ 111 | a: null 112 | ] 113 | def actual = [ 114 | a: 'refrigerator' 115 | ] 116 | 117 | when: 118 | def result = match(expected, actual) 119 | 120 | then: 121 | result.message() == 'Mismatch at path: \$.a\nExpected null. Found: "refrigerator"' 122 | print result 123 | } 124 | 125 | def "ordered array expectation: unexpected elements"() { 126 | given: 127 | def expected = [ 128 | a: ['red', 'green', 'blue'] 129 | ] 130 | def actual = [ 131 | a: ['red', 'green', 'blue', 'silver', 'black'] 132 | ] 133 | 134 | when: 135 | def result = match(expected, actual) 136 | 137 | then: 138 | result.message() == 'Mismatch at path: \$.a\nArray contains unexpected items: ["silver", "black"]' 139 | print result 140 | } 141 | 142 | def "ordered array expectation: lacking elements"() { 143 | given: 144 | def expected = [ 145 | a: ['red', 'green', 'blue', 'silver', 'black'] 146 | ] 147 | def actual = [ 148 | a: ['red', 'green'] 149 | ] 150 | 151 | when: 152 | def result = match(expected, actual) 153 | 154 | then: 155 | result.message() == 'Mismatch at path: \$.a\nArray lacks the items: ["blue", "silver", "black"]' 156 | print result 157 | } 158 | 159 | def "ordered array expectation: unexpected element as object"() { 160 | given: 161 | def expected = [ 162 | items: [ 163 | [firstname: 'Jack', lastname: 'Bauer'], 164 | [firstname: 'Franz', lastname: 'Beckenbauer'] 165 | ] 166 | ] 167 | def actual = [ 168 | items: [ 169 | [firstname: 'Jack', lastname: 'Bauer'], 170 | [firstname: 'Franz', lastname: 'Beckenbauer'], 171 | [firstname: 'Oliver', lastname: 'Twist', position: [x: 1, y: 1]] 172 | ] 173 | ] 174 | 175 | when: 176 | def result = match(expected, actual) 177 | 178 | then: 179 | result.message() == 'Mismatch at path: \$.items\nArray contains unexpected items: [{"firstname": "Oliver", "lastname": "Twist", "position": {"x": 1, "y": 1}}]' 180 | print result 181 | } 182 | 183 | def "ordered array expectation: unexpected element as array"() { 184 | given: 185 | def expected = [ 186 | items: [ 187 | ['red', 'green', 'blue'], 188 | ['cyan', 'magenta', 'yellow'] 189 | ] 190 | ] 191 | def actual = [ 192 | items: [ 193 | ['red', 'green', 'blue'], 194 | ['cyan', 'magenta', 'yellow'], 195 | ['huehue', 'hue', 'alpha', 'saturn', 'jupiter'], 196 | ['finito'] 197 | ] 198 | ] 199 | 200 | when: 201 | def result = match(expected, actual) 202 | 203 | then: 204 | result.message() == 'Mismatch at path: \$.items\nArray contains unexpected items: [["huehue", "hue", "alpha", "saturn", "jupiter"], ["finito"]]' 205 | print result 206 | } 207 | 208 | MatchResult match(Map expected, Map actual) { 209 | matcherFactory.matcher() 210 | .expected(expected) 211 | .actual(toJson(actual)) 212 | .match() 213 | } 214 | 215 | private static void print(MatchResult result) { 216 | println result.message() + '\n' 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /jazon-core/src/test/groovy/com/zendesk/jazon/MismatchPathSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon 2 | 3 | import com.zendesk.jazon.actual.factory.GsonActualFactory 4 | import com.zendesk.jazon.expectation.translator.DefaultTranslators 5 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators 6 | import com.zendesk.jazon.expectation.translator.TranslatorFacade 7 | import com.zendesk.jazon.mismatch.impl.PrimitiveValueMismatch 8 | import spock.lang.Specification 9 | 10 | import static groovy.json.JsonOutput.toJson 11 | 12 | class MismatchPathSpec extends Specification { 13 | private static final TestActualFactory testActualFactory = new TestActualFactory() 14 | private static final MatcherFactory matcherFactory = new MatcherFactory( 15 | new TranslatorFacade(DefaultTranslators.translators() + JazonTypesTranslators.translators()), 16 | new GsonActualFactory() 17 | ) 18 | 19 | def "complex case"() { 20 | given: 21 | def expected = [ 22 | data: [ 23 | [key: 111, values: [a: 1, b: 2]], 24 | [key: 111, values: [a: 2, b: 4]], 25 | [key: 111, values: [a: 3, b: 8]], 26 | [key: 111, values: [a: 4, b: 16]], 27 | ] 28 | ] 29 | def actual = [ 30 | data: [ 31 | [key: 111, values: [a: 1, b: 2]], 32 | [key: 111, values: [a: 2, b: 4]], 33 | [key: 111, values: [a: 3, b: 99]], 34 | [key: 111, values: [a: 4, b: 16]], 35 | ] 36 | ] 37 | 38 | when: 39 | def result = match(expected, actual) 40 | 41 | then: 42 | !result.ok() 43 | result.mismatch().expectationMismatch() == primitiveValueMismatch(8, 99) 44 | result.mismatch().path() == '$.data.2.values.b' 45 | } 46 | 47 | def "complex case 2"() { 48 | given: 49 | def expected = VERY_COMPLEX_OBJECT 50 | def actual = VERY_COMPLEX_OBJECT_WITH_ONE_FIELD_DIFFERENT 51 | 52 | when: 53 | def result = match(expected, actual) 54 | 55 | then: 56 | !result.ok() 57 | result.mismatch().expectationMismatch() == primitiveValueMismatch("green", "black") 58 | result.mismatch().path() == '$.data.1.values.elements.0.value' 59 | } 60 | 61 | private static MatchResult match(Map expected, Map actual) { 62 | matcherFactory.matcher() 63 | .expected(expected) 64 | .actual(toJson(actual)) 65 | .match() 66 | } 67 | 68 | private static PrimitiveValueMismatch primitiveValueMismatch(def expected, def actual) { 69 | return new PrimitiveValueMismatch(testActualFactory.actual(expected), testActualFactory.actual(actual)) 70 | } 71 | 72 | private static final VERY_COMPLEX_OBJECT = [ 73 | data: [ 74 | [ 75 | key : 111, 76 | values: [ 77 | a : 1, 78 | b : "one", 79 | elements: [ 80 | [ 81 | name: "color", 82 | value: "blue" 83 | ], 84 | [ 85 | name: "width", 86 | value: 5 87 | ], 88 | [ 89 | name: "height", 90 | value: 10 91 | ], 92 | ] 93 | ] 94 | ], 95 | [ 96 | key : 111, 97 | values: [ 98 | a: 2, 99 | b: "two", 100 | elements: [ 101 | [ 102 | name: "color", 103 | value: "green" 104 | ], 105 | [ 106 | name: "width", 107 | value: 12 108 | ], 109 | [ 110 | name: "height", 111 | value: 24 112 | ], 113 | ] 114 | ] 115 | ], 116 | [ 117 | key : 111, 118 | values: [ 119 | a: 3, 120 | b: "three", 121 | elements: [ 122 | [ 123 | name: "color", 124 | value: "red" 125 | ], 126 | [ 127 | name: "width", 128 | value: 33 129 | ], 130 | [ 131 | name: "height", 132 | value: 66 133 | ], 134 | ] 135 | ] 136 | ], 137 | [ 138 | key : 111, 139 | values: [ 140 | a: 4, 141 | b: "four", 142 | elements: [ 143 | [ 144 | name: "color", 145 | value: "orange" 146 | ], 147 | [ 148 | name: "width", 149 | value: 25 150 | ], 151 | [ 152 | name: "height", 153 | value: 50 154 | ], 155 | ] 156 | ] 157 | ], 158 | ] 159 | ] 160 | 161 | private static final VERY_COMPLEX_OBJECT_WITH_ONE_FIELD_DIFFERENT = [ 162 | data: [ 163 | [ 164 | key : 111, 165 | values: [ 166 | a : 1, 167 | b : "one", 168 | elements: [ 169 | [ 170 | name: "color", 171 | value: "blue" 172 | ], 173 | [ 174 | name: "width", 175 | value: 5 176 | ], 177 | [ 178 | name: "height", 179 | value: 10 180 | ], 181 | ] 182 | ] 183 | ], 184 | [ 185 | key : 111, 186 | values: [ 187 | a: 2, 188 | b: "two", 189 | elements: [ 190 | [ 191 | name: "color", 192 | value: "black" 193 | ], 194 | [ 195 | name: "width", 196 | value: 12 197 | ], 198 | [ 199 | name: "height", 200 | value: 24 201 | ], 202 | ] 203 | ] 204 | ], 205 | [ 206 | key : 111, 207 | values: [ 208 | a: 3, 209 | b: "three", 210 | elements: [ 211 | [ 212 | name: "color", 213 | value: "red" 214 | ], 215 | [ 216 | name: "width", 217 | value: 33 218 | ], 219 | [ 220 | name: "height", 221 | value: 66 222 | ], 223 | ] 224 | ] 225 | ], 226 | [ 227 | key : 111, 228 | values: [ 229 | a: 4, 230 | b: "four", 231 | elements: [ 232 | [ 233 | name: "color", 234 | value: "orange" 235 | ], 236 | [ 237 | name: "width", 238 | value: 25 239 | ], 240 | [ 241 | name: "height", 242 | value: 50 243 | ], 244 | ] 245 | ] 246 | ], 247 | ] 248 | ] 249 | } 250 | -------------------------------------------------------------------------------- /jazon-core/src/test/groovy/com/zendesk/jazon/actual/ActualJsonNumberSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.actual 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | class ActualJsonNumberSpec extends Specification { 7 | 8 | def "equals() returns true, hashCode() equals"() { 9 | given: 10 | def fooNumber = new ActualJsonNumber(foo as Number) 11 | def barNumber = new ActualJsonNumber(bar as Number) 12 | 13 | expect: 14 | fooNumber.equals(barNumber) 15 | fooNumber.hashCode() == barNumber.hashCode() 16 | 17 | and: 18 | barNumber.equals(fooNumber) 19 | barNumber.hashCode() == fooNumber.hashCode() 20 | 21 | where: 22 | foo | bar 23 | 1 as int | 1 as int 24 | 1 as long | 1 as long 25 | 1 as long | 1 as int 26 | 1.1d | 1.1d 27 | 1.1f | 1.1f 28 | new BigDecimal("1.1") | new BigDecimal("1.1") 29 | } 30 | 31 | @Unroll 32 | def "hashCode() differs and equals() returns false"() { 33 | given: 34 | def fooNumber = new ActualJsonNumber(foo) 35 | def barNumber = new ActualJsonNumber(bar) 36 | 37 | expect: 38 | fooNumber.hashCode() != barNumber.hashCode() 39 | !fooNumber.equals(barNumber) 40 | 41 | and: 42 | barNumber.hashCode() != fooNumber.hashCode() 43 | !barNumber.equals(fooNumber) 44 | 45 | where: 46 | foo | bar 47 | 1 as int | 2 as int 48 | 1 as long | 2 as long 49 | 1 as int | 2 as long 50 | 1 as long | 2 as int 51 | 1.2f | 1 as int 52 | 1.2d | 1 as int 53 | 1.2f | 1.4f 54 | 1.2f | 1.4d 55 | new BigDecimal('1.000000000000001') | new BigDecimal('1.0000000000000011') 56 | 1.000000000000001d | new BigDecimal('1.0000000000000011') 57 | 1.000000000000001f | new BigDecimal('1.0000000000000011') 58 | new BigDecimal('1.000000000000000000000000001') | new BigDecimal('1.0000000000000000000000000011') 59 | 1.1f | new BigDecimal('1.1') // Doesn't match because the types differ. 60 | 1.1d | new BigDecimal('1.1') // Doesn't match because the types differ. 61 | 1 as int | 1.0f // Doesn't match because the types differ. 62 | 1 as int | 1.0d // Doesn't match because the types differ. 63 | 1 as long | 1.0f // Doesn't match because the types differ. 64 | 1 as long | 1.0d // Doesn't match because the types differ. 65 | 1.1f | 1.1d // Doesn't match because the types differ. 66 | } 67 | 68 | def "hashCode() equals and equals() returns false"() { 69 | given: 70 | def fooNumber = new ActualJsonNumber(foo) 71 | def barNumber = new ActualJsonNumber(bar) 72 | 73 | expect: 74 | fooNumber.hashCode() == barNumber.hashCode() 75 | !fooNumber.equals(barNumber) 76 | 77 | and: 78 | barNumber.hashCode() == fooNumber.hashCode() 79 | !barNumber.equals(fooNumber) 80 | 81 | where: 82 | foo | bar 83 | ((long) Integer.MAX_VALUE + 1) as long | Integer.MIN_VALUE as int 84 | ((long) Integer.MAX_VALUE + 1) as long | Integer.MIN_VALUE as long 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /jazon-core/src/test/groovy/com/zendesk/jazon/mismatch/TypeMismatchSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.mismatch 2 | 3 | import com.zendesk.jazon.actual.ActualJsonArray 4 | import com.zendesk.jazon.actual.ActualJsonBoolean 5 | import com.zendesk.jazon.actual.ActualJsonNumber 6 | import com.zendesk.jazon.actual.ActualJsonObject 7 | import com.zendesk.jazon.actual.ActualJsonString 8 | import com.zendesk.jazon.mismatch.impl.TypeMismatch 9 | import spock.lang.Specification 10 | 11 | class TypeMismatchSpec extends Specification { 12 | 13 | def "displays good message"() { 14 | given: 15 | def mismatch = new TypeMismatch(expectedType, actualType) 16 | 17 | expect: 18 | mismatch.message() == "Expected type: $expectedTypeAsString\nActual type: $actualTypeAsString" 19 | println mismatch.message() + '\n' 20 | 21 | where: 22 | expectedType | actualType || expectedTypeAsString | actualTypeAsString 23 | ActualJsonObject | ActualJsonArray || 'Object' | 'Array' 24 | ActualJsonArray | ActualJsonNumber || 'Array' | 'Number' 25 | ActualJsonNumber | ActualJsonString || 'Number' | 'String' 26 | ActualJsonBoolean | ActualJsonObject || 'Boolean' | 'Object' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /jazon-core/src/test/java/com/zendesk/jazon/TestActualFactory.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon; 2 | 3 | import com.zendesk.jazon.actual.Actual; 4 | import com.zendesk.jazon.actual.factory.ActualFactory; 5 | import com.zendesk.jazon.actual.ActualJsonArray; 6 | import com.zendesk.jazon.actual.ActualJsonBoolean; 7 | import com.zendesk.jazon.actual.ActualJsonNull; 8 | import com.zendesk.jazon.actual.ActualJsonNumber; 9 | import com.zendesk.jazon.actual.ActualJsonObject; 10 | import com.zendesk.jazon.actual.ActualJsonString; 11 | 12 | import java.util.LinkedHashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static java.util.stream.Collectors.toList; 17 | import static java.util.stream.Collectors.toMap; 18 | 19 | /** 20 | * This class exists to handle assertions in tests when instance of Actual is needed in the assertion-expectation - e.g. 21 | * equality-comparing of `Mismatch` instances that store Actual instance inside itself. 22 | * Without this class we would have to write lot of verbose constructor-calls like `new ActualJsonArray(...)` etc. 23 | */ 24 | public class TestActualFactory implements ActualFactory { 25 | 26 | @Override 27 | public Actual actual(Object input) { 28 | if (input instanceof Map) { 29 | return actualObject((Map) input); 30 | } else if (input instanceof Number) { 31 | 32 | return new ActualJsonNumber((Number) input); 33 | } else if (input instanceof String) { 34 | return new ActualJsonString((String) input); 35 | } else if (input == null) { 36 | return ActualJsonNull.INSTANCE; 37 | } else if (input instanceof List) { 38 | return actualArray((List) input); 39 | } else if (input instanceof Boolean) { 40 | return new ActualJsonBoolean((Boolean) input); 41 | } 42 | throw new IllegalArgumentException(); 43 | } 44 | 45 | private ActualJsonObject actualObject(Map objectsMap) { 46 | Map map = objectsMap.entrySet() 47 | .stream() 48 | .collect( 49 | toMap( 50 | Map.Entry::getKey, 51 | e -> actual(e.getValue()), 52 | (a, b) -> a, 53 | LinkedHashMap::new 54 | ) 55 | ); 56 | return new ActualJsonObject(map); 57 | } 58 | 59 | private ActualJsonArray actualArray(List objects) { 60 | List actuals = objects.stream() 61 | .map(this::actual) 62 | .collect(toList()); 63 | return new ActualJsonArray(actuals); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /jazon-junit/README.md: -------------------------------------------------------------------------------- 1 | # Jazon JUnit 2 | A library for test assertions on JSON payloads - for JUnit framework. 3 | 4 | ## Quickstart 5 | 6 | #### Example 1: Simple exact-match 7 | 8 | For such JSON: 9 | ```json 10 | { 11 | "firstname": "Steve", 12 | "lastname": "Jobs" 13 | } 14 | ``` 15 | 16 | ... an exact-match assertion would look like this: 17 | 18 | ```java 19 | @Test 20 | public void simpleTest() { 21 | // when 22 | String actualJson = getSteveJobsJson(); 23 | 24 | // then 25 | assertThat(response).matches( 26 | new JazonMap() 27 | .with("firstname", "Steve") 28 | .with("lastname", "Jobs") 29 | ); 30 | } 31 | ``` 32 | 33 | #### Example 2: Unordered array 34 | 35 | This assertion passes even though the items in the array are in different order. 36 | 37 | The `actualJson`: 38 | ```json 39 | { 40 | "id": 95478, 41 | "name": "Coca Cola", 42 | "tags": ["sprite", "pepsi", "7up", "fanta", "dr pepper"] 43 | } 44 | ``` 45 | 46 | The assertion: 47 | ```java 48 | assertThat(response).matches( 49 | new JazonMap() 50 | .with("id", 95478) 51 | .with("name", "Coca Cola") 52 | .with("tags", set("pepsi", "dr pepper", "sprite", "fanta", "7up")) 53 | ); 54 | ``` 55 | ```java 56 | private Set set(Object... elements) { 57 | HashSet result = new HashSet<>(elements.length); 58 | result.addAll(asList(elements)); 59 | return result; 60 | } 61 | ``` 62 | 63 | #### Example 3: Custom assertions 64 | 65 | If you need, instead of exact-matching, you can define custom assertions using Predicates. 66 | Here for example, we used custom assertions: 67 | * to check that a number is in given range - `(Integer id) -> id >= 0` 68 | * to check that a field matches a regex - `regex()` 69 | * to check that a field just exists, no matter of its value - `Objects::nonNull` 70 | 71 | The `actualJson`: 72 | ```json 73 | { 74 | "id": 95478, 75 | "name": "Coca Cola", 76 | "value": "133.30", 77 | "updated_at": "1990-06-19T12:19:10Z" 78 | } 79 | ``` 80 | 81 | The assertion: 82 | ```java 83 | assertThat(response).matches( 84 | new JazonMap() 85 | .with("id", (Integer id) -> id >= 0) 86 | .with("name", "Coca Cola") 87 | .with("value", regex("\\d+\\.\\d\\d")) 88 | .with("updated_at", Objects::nonNull) 89 | ); 90 | ``` 91 | 92 | ```java 93 | private Predicate regex(String regex) { 94 | return value -> value.matches(regex); 95 | } 96 | ``` 97 | 98 | #### Example 4: Utils extraction 99 | 100 | To avoid code duplication, you can extract your common wildcard-assertions to constants. 101 | 102 | ```java 103 | assertThat(response).matches( 104 | new JazonMap() 105 | .with("id", ANY_ID) // a constant 106 | .with("name", "Coca Cola") 107 | .with("value", "133.30") 108 | .with("updated_at", ANY_ISO_DATETIME) // a constant 109 | ); 110 | ``` 111 | ```java 112 | private static final Predicate ANY_ID = (id) -> id >= 0; 113 | private static final Predicate ANY_ISO_DATETIME = 114 | datetime -> datetime.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"); 115 | ``` 116 | 117 | #### Example 5: Utils extraction to domain objects 118 | 119 | To avoid code duplication even more, you can extract the parts of JSON. This will also 120 | make your tests more readable. 121 | Here in the examples we extract `deal()` - a business object from sales domain. 122 | 123 | ```java 124 | assertThat(response).matches(deal("Coca Cola", "10.00")); 125 | ``` 126 | ```java 127 | assertThat(response).matches( 128 | asList( 129 | deal("Coca Cola", "10.00"), 130 | deal("Pepsi", "9.00"), 131 | deal("Fanta", "10.00"), 132 | deal("Sprite", "10.00"), 133 | deal("Dr Pepper", "12.00") 134 | ) 135 | ); 136 | ``` 137 | ```java 138 | private JazonMap deal(String name, String value) { 139 | return new JazonMap() 140 | .with("id", ANY_ID) 141 | .with("name", name) 142 | .with("value", value) 143 | .with("updated_at", ANY_ISO_DATETIME); 144 | } 145 | ``` 146 | 147 | #### Examples code 148 | You can check out the example tests [in the code](/examples/src/test/java/com/zendesk/jazon/junit/ReadmeExamplesTest.java) 149 | 150 | ## Copyright and license 151 | Copyright 2019 Zendesk, Inc. 152 | 153 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 154 | You may obtain a copy of the License at 155 | http://www.apache.org/licenses/LICENSE-2.0 156 | 157 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 158 | 159 | -------------------------------------------------------------------------------- /jazon-junit/build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | name = 'Jazon JUnit Adapter' 3 | description = 'A library for test assertions on JSON payloads - for JUnit framework.' 4 | } 5 | 6 | apply from: '../gradle/publishing.gradle' 7 | 8 | dependencies { 9 | compile project(':jazon-core') 10 | 11 | compileOnly 'org.projectlombok:lombok:1.18.12' 12 | annotationProcessor 'org.projectlombok:lombok:1.18.12' 13 | 14 | testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.1' 15 | } 16 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/expectation/translator/JunitTranslators.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.expectation.JsonExpectation; 4 | import com.zendesk.jazon.expectation.impl.ObjectExpectation; 5 | import com.zendesk.jazon.expectation.impl.PredicateExpectation; 6 | import com.zendesk.jazon.junit.JazonMap; 7 | import com.zendesk.jazon.junit.JsonExpectationInput; 8 | import com.zendesk.jazon.junit.ObjectExpectationInput; 9 | import com.zendesk.jazon.junit.PredicateExpectationInput; 10 | 11 | import java.util.HashMap; 12 | import java.util.LinkedHashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static java.util.Arrays.asList; 17 | import static java.util.stream.Collectors.toMap; 18 | 19 | public class JunitTranslators { 20 | public static List> translators() { 21 | return asList( 22 | new TranslatorMapping<>(ObjectExpectationInput.class, new ObjectExpectationInputTranslator()), 23 | new TranslatorMapping<>(PredicateExpectationInput.class, new PredicateExpectationInputTranslator()), 24 | new TranslatorMapping<>(JazonMap.class, new JazonMapTranslator()) 25 | ); 26 | } 27 | 28 | private static class ObjectExpectationInputTranslator implements Translator { 29 | @Override 30 | public JsonExpectation jsonExpectation(ObjectExpectationInput objectExpectationInput, TranslatorFacade translator) { 31 | return translator.expectation(objectExpectationInput.object()); 32 | } 33 | } 34 | 35 | private static class PredicateExpectationInputTranslator implements Translator { 36 | @Override 37 | public JsonExpectation jsonExpectation(PredicateExpectationInput predicateExpectationInput, TranslatorFacade translator) { 38 | return new PredicateExpectation(predicateExpectationInput.predicate()); 39 | } 40 | } 41 | 42 | private static class JazonMapTranslator implements Translator { 43 | @Override 44 | public JsonExpectation jsonExpectation(JazonMap jazonMap, TranslatorFacade translator) { 45 | HashMap map = copied(jazonMap.map()); 46 | //FIXME code duplication 47 | LinkedHashMap expectationsMap = map.entrySet() 48 | .stream() 49 | .collect( 50 | toMap( 51 | e -> e.getKey().toString(), 52 | e -> translator.expectation(e.getValue()), 53 | (a, b) -> a, 54 | () -> new LinkedHashMap<>(map.size()) 55 | ) 56 | ); 57 | return new ObjectExpectation(expectationsMap); 58 | } 59 | 60 | /** 61 | * Returns Map converted from Map 62 | */ 63 | private HashMap copied(Map map) { 64 | return map 65 | .entrySet() 66 | .stream() 67 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, HashMap::new)); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/junit/JazonJunitAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.MatcherFactory; 5 | import com.zendesk.jazon.actual.factory.GsonActualFactory; 6 | import com.zendesk.jazon.expectation.translator.DefaultTranslators; 7 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators; 8 | import com.zendesk.jazon.expectation.translator.JunitTranslators; 9 | import com.zendesk.jazon.expectation.translator.TranslatorFacade; 10 | import com.zendesk.jazon.expectation.translator.TranslatorMapping; 11 | 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | 15 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 16 | 17 | public class JazonJunitAdapter { 18 | private static final MatcherFactory matcherFactory = new MatcherFactory( 19 | new TranslatorFacade(translators()), 20 | new GsonActualFactory() 21 | ); 22 | 23 | private final String actualJson; 24 | 25 | public JazonJunitAdapter(String actualJson) { 26 | this.actualJson = checkNotNull(actualJson); 27 | } 28 | 29 | public static JazonJunitAdapter assertThat(String actualJson) { 30 | return new JazonJunitAdapter(actualJson); 31 | } 32 | 33 | public void matches(JazonMap jazonMap) { 34 | matchExpectedObject(jazonMap.map()); 35 | } 36 | 37 | public void matches(Object expected) { 38 | matchExpectedObject(expected); 39 | } 40 | 41 | private void matchExpectedObject(Object expected) { 42 | MatchResult matchResult = matcherFactory.matcher() 43 | .expected(expected) 44 | .actual(actualJson) 45 | .match(); 46 | if (matchResult.ok()) { 47 | return; 48 | } 49 | String mismatchMessageTemplate = "\n-----------------------------------\nJSON MISMATCH:\n%s\n-----------------------------------\n"; 50 | throw new AssertionError(String.format(mismatchMessageTemplate, matchResult.message())); 51 | } 52 | 53 | private static List> translators() { 54 | List> result = new LinkedList<>(); 55 | result.addAll(DefaultTranslators.translators()); 56 | result.addAll(JazonTypesTranslators.translators()); 57 | result.addAll(JunitTranslators.translators()); 58 | return result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/junit/JazonList.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import lombok.EqualsAndHashCode; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.function.Predicate; 8 | 9 | import static com.zendesk.jazon.util.Preconditions.checkNotNull; 10 | 11 | /** 12 | * This class exists to allow to pass a lambda-predicate to the same interface as other typical objects like String, 13 | * Integer, List, etc. are passed. This is due to the limitation that {@code Object} is not effectively a supertype of 14 | * lambda expression. 15 | */ 16 | @EqualsAndHashCode 17 | public class JazonList { 18 | private final List list = new ArrayList<>(); 19 | 20 | public JazonList(Predicate... predicates) { 21 | checkNotNull(predicates); 22 | for (Predicate element : predicates) { 23 | list.add(new PredicateExpectationInput<>(element)); 24 | } 25 | } 26 | 27 | public JazonList(Object... objects) { 28 | checkNotNull(objects); 29 | for (Object element : objects) { 30 | list.add(new ObjectExpectationInput(element)); 31 | } 32 | } 33 | 34 | public JazonList with(Object object) { 35 | list.add(new ObjectExpectationInput(object)); 36 | return this; 37 | } 38 | 39 | public JazonList with(Predicate predicate) { 40 | list.add(new PredicateExpectationInput<>(predicate)); 41 | return this; 42 | } 43 | 44 | public List list() { 45 | return list; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/junit/JazonMap.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import lombok.EqualsAndHashCode; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.function.Predicate; 8 | 9 | /** 10 | * This class exists to allow to pass a lambda-predicate to the same interface as other typical objects like String, 11 | * Integer, List, etc. are passed. This is due to the limitation that {@code Object} is not effectively a supertype of 12 | * lambda expression. 13 | */ 14 | @EqualsAndHashCode 15 | public class JazonMap { 16 | private final Map map = new HashMap<>(); 17 | 18 | public JazonMap with(String fieldName, Object fieldValue) { 19 | map.put(fieldName, new ObjectExpectationInput(fieldValue)); 20 | return this; 21 | } 22 | 23 | public JazonMap with(String fieldName, Predicate predicate) { 24 | map.put(fieldName, new PredicateExpectationInput<>(predicate)); 25 | return this; 26 | } 27 | 28 | public Map map() { 29 | return map; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/junit/JsonExpectationInput.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | /** 4 | * A marker interface. It exists only to have a common supertype to store the instances of its subtypes in a single 5 | * collection. 6 | */ 7 | public interface JsonExpectationInput { 8 | // intentionally left blank 9 | } 10 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/junit/ObjectExpectationInput.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.ToString; 5 | 6 | @ToString 7 | @EqualsAndHashCode 8 | public class ObjectExpectationInput implements JsonExpectationInput { 9 | private final Object object; 10 | 11 | ObjectExpectationInput(Object object) { 12 | this.object = object; 13 | } 14 | 15 | public Object object() { 16 | return object; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /jazon-junit/src/main/java/com/zendesk/jazon/junit/PredicateExpectationInput.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.ToString; 5 | 6 | import java.util.function.Predicate; 7 | 8 | @ToString 9 | @EqualsAndHashCode 10 | public class PredicateExpectationInput implements JsonExpectationInput { 11 | private final Predicate predicate; 12 | 13 | PredicateExpectationInput(Predicate predicate) { 14 | this.predicate = predicate; 15 | } 16 | 17 | public Predicate predicate() { 18 | return predicate; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /jazon-junit/src/test/java/com/zendesk/jazon/junit/JazonListTest.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.function.Predicate; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class JazonListTest { 14 | 15 | @Test 16 | void onlySimpleTypes() { 17 | // given 18 | JazonList jazonList = new JazonList("orange", 55, false, 173.50, null); 19 | 20 | // when 21 | List list = jazonList.list(); 22 | 23 | // then 24 | assertEquals(5, list.size()); 25 | assertEquals(new ObjectExpectationInput("orange"), list.get(0)); 26 | assertEquals(new ObjectExpectationInput(55), list.get(1)); 27 | assertEquals(new ObjectExpectationInput(false), list.get(2)); 28 | assertEquals(new ObjectExpectationInput(173.50), list.get(3)); 29 | assertEquals(new ObjectExpectationInput(null), list.get(4)); 30 | } 31 | 32 | @Test 33 | void nullIsTranslatedToNullPredicate_usingConstructor() { 34 | // given 35 | JazonList jazonList = new JazonList(null, null); 36 | 37 | // when 38 | List list = jazonList.list(); 39 | 40 | // then 41 | assertEquals(2, list.size()); 42 | assertEquals(new PredicateExpectationInput<>(null), list.get(0)); 43 | } 44 | 45 | @Test 46 | void nullIsTranslatedToNullPredicate_usingModifierMethod() { 47 | // given 48 | JazonList jazonList = new JazonList().with(null); 49 | 50 | // when 51 | List list = jazonList.list(); 52 | 53 | // then 54 | assertEquals(1, list.size()); 55 | assertEquals(new PredicateExpectationInput<>(null), list.get(0)); 56 | } 57 | 58 | @Test 59 | void failsForAloneNullInConstructor() { 60 | assertThrows(NullPointerException.class, () -> new JazonList(null)); 61 | } 62 | 63 | @Test 64 | void onlyPredicates() { 65 | // given 66 | JazonList jazonList = new JazonList((Integer it) -> it > 0, (String s) -> s.matches("re.*")); 67 | 68 | // when 69 | List list = jazonList.list(); 70 | 71 | // then 72 | assertEquals(2, list.size()); 73 | PredicateExpectationInput firstPredicateInput = (PredicateExpectationInput) list.get(0); 74 | Predicate firstPredicate = firstPredicateInput.predicate(); 75 | assertFalse(firstPredicate.test(-10)); 76 | assertFalse(firstPredicate.test(-1)); 77 | assertFalse(firstPredicate.test(0)); 78 | assertTrue(firstPredicate.test(1)); 79 | assertTrue(firstPredicate.test(10)); 80 | 81 | PredicateExpectationInput secondPredicateInput = (PredicateExpectationInput) list.get(1); 82 | Predicate secondPredicate = secondPredicateInput.predicate(); 83 | assertTrue(secondPredicate.test("red")); 84 | assertTrue(secondPredicate.test("reindeer")); 85 | assertFalse(secondPredicate.test("black")); 86 | assertFalse(secondPredicate.test("octopus")); 87 | } 88 | 89 | @Test 90 | void nestedMap() { 91 | // given 92 | JazonMap robert = new JazonMap() 93 | .with("firstname", "Robert") 94 | .with("firstname", "Kubica"); 95 | JazonMap jenson = new JazonMap() 96 | .with("firstname", "Jenson") 97 | .with("firstname", "Button"); 98 | JazonList jazonList = new JazonList(robert, jenson); 99 | 100 | // when 101 | List list = jazonList.list(); 102 | 103 | // then 104 | assertEquals(2, list.size()); 105 | assertEquals(new ObjectExpectationInput(robert), list.get(0)); 106 | assertEquals(new ObjectExpectationInput(jenson), list.get(1)); 107 | } 108 | 109 | @Test 110 | void nestedList() { 111 | // given 112 | JazonList drinks = new JazonList("pepsi", "coca cola", "sprite"); 113 | JazonList birds = new JazonList("pigeon", "sparrow"); 114 | JazonList jazonList = new JazonList(drinks, birds); 115 | 116 | // when 117 | List list = jazonList.list(); 118 | 119 | // then 120 | assertEquals(2, list.size()); 121 | assertEquals(new ObjectExpectationInput(drinks), list.get(0)); 122 | assertEquals(new ObjectExpectationInput(birds), list.get(1)); 123 | } 124 | 125 | @Test 126 | void allThingsAtOnce() { 127 | // given 128 | JazonMap nestedMap = new JazonMap() 129 | .with("firstname", "Robert") 130 | .with("lastname", "Kubica"); 131 | JazonList drinks = new JazonList("pepsi", "coca cola", "sprite"); 132 | JazonList jazonList = new JazonList() 133 | .with(150) 134 | .with(nestedMap) 135 | .with((Integer it) -> it > 100) 136 | .with("orange") 137 | .with(null) 138 | .with(drinks) 139 | .with(false); 140 | 141 | // when 142 | List list = jazonList.list(); 143 | 144 | // then 145 | assertEquals(7, list.size()); 146 | assertEquals(new ObjectExpectationInput(150), list.get(0)); 147 | assertEquals(new ObjectExpectationInput(nestedMap), list.get(1)); 148 | 149 | PredicateExpectationInput predicateInput = (PredicateExpectationInput) list.get(2); 150 | Predicate firstPredicate = predicateInput.predicate(); 151 | assertFalse(firstPredicate.test(-1)); 152 | assertFalse(firstPredicate.test(0)); 153 | assertFalse(firstPredicate.test(10)); 154 | assertFalse(firstPredicate.test(100)); 155 | assertTrue(firstPredicate.test(101)); 156 | assertTrue(firstPredicate.test(1000)); 157 | 158 | assertEquals(new ObjectExpectationInput("orange"), list.get(3)); 159 | assertEquals(new PredicateExpectationInput<>(null), list.get(4)); 160 | assertEquals(new ObjectExpectationInput(drinks), list.get(5)); 161 | assertEquals(new ObjectExpectationInput(false), list.get(6)); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /jazon-junit/src/test/java/com/zendesk/jazon/junit/JazonMapTest.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Map; 6 | import java.util.function.Predicate; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class JazonMapTest { 13 | 14 | @Test 15 | void simpleMap() { 16 | // given 17 | JazonMap jazonMap = new JazonMap() 18 | .with("name", "Leo Messi") 19 | .with("email", "leo@messi.com") 20 | .with("height", 170) 21 | .with("nickname", null) 22 | .with("is_cool", true); 23 | 24 | // when 25 | Map map = jazonMap.map(); 26 | 27 | // then 28 | assertEquals(5, map.size()); 29 | assertEquals(new ObjectExpectationInput("Leo Messi"), map.get("name")); 30 | assertEquals(new ObjectExpectationInput("leo@messi.com"), map.get("email")); 31 | assertEquals(new ObjectExpectationInput(170), map.get("height")); 32 | assertEquals(new PredicateExpectationInput<>(null), map.get("nickname")); 33 | assertEquals(new ObjectExpectationInput(true), map.get("is_cool")); 34 | } 35 | 36 | @Test 37 | void mapWithNestedMap() { 38 | // given 39 | JazonMap nestedJazonMap = new JazonMap() 40 | .with("firstname", "Fernando") 41 | .with("lastname", "Alonso"); 42 | JazonMap jazonMap = new JazonMap() 43 | .with("driver", nestedJazonMap); 44 | 45 | // when 46 | Map map = jazonMap.map(); 47 | 48 | // then 49 | assertEquals(1, map.size()); 50 | assertEquals(new ObjectExpectationInput(nestedJazonMap), map.get("driver")); 51 | } 52 | 53 | @Test 54 | void mapWithNestedLists() { 55 | // given 56 | JazonList drinks = new JazonList("pepsi", "coca cola", "sprite", "fanta"); 57 | JazonList birds = new JazonList("pigeon", "sparrow"); 58 | JazonMap jazonMap = new JazonMap() 59 | .with("birds", birds) 60 | .with("drinks", drinks); 61 | 62 | // when 63 | Map map = jazonMap.map(); 64 | 65 | // then 66 | assertEquals(2, map.size()); 67 | assertEquals(new ObjectExpectationInput(birds), map.get("birds")); 68 | assertEquals(new ObjectExpectationInput(drinks), map.get("drinks")); 69 | } 70 | 71 | @Test 72 | void mapWithPredicate() { 73 | // given 74 | JazonMap jazonMap = new JazonMap() 75 | .with("id", (Integer id) -> id > 0); 76 | 77 | // when 78 | Map map = jazonMap.map(); 79 | 80 | // then 81 | assertEquals(1, map.size()); 82 | PredicateExpectationInput predicateExpectationInput = (PredicateExpectationInput) map.get("id"); 83 | Predicate predicate = predicateExpectationInput.predicate(); 84 | assertFalse(predicate.test(-10)); 85 | assertFalse(predicate.test(-1)); 86 | assertFalse(predicate.test(0)); 87 | assertTrue(predicate.test(1)); 88 | assertTrue(predicate.test(10)); 89 | } 90 | 91 | @Test 92 | void mapWithAllTheThingsAtOnce() { 93 | // given 94 | JazonMap driverMap = new JazonMap() 95 | .with("firstname", "Fernando") 96 | .with("lastname", "Alonso"); 97 | JazonList drinksList = new JazonList("pepsi", "coca cola", "sprite", "fanta"); 98 | JazonMap jazonMap = new JazonMap() 99 | .with("name", "Leo Messi") 100 | .with("email", "leo@messi.com") 101 | .with("nickname", null) 102 | .with("height", 170) 103 | .with("drinks", drinksList) 104 | .with("is_cool", true) 105 | .with("id", (Integer id) -> id > 0) 106 | .with("driver", driverMap); 107 | 108 | // when 109 | Map map = jazonMap.map(); 110 | 111 | // then 112 | assertEquals(8, map.size()); 113 | assertEquals(map.get("name"), new ObjectExpectationInput("Leo Messi")); 114 | assertEquals(map.get("email"), new ObjectExpectationInput("leo@messi.com")); 115 | assertEquals(map.get("nickname"), new PredicateExpectationInput<>(null)); 116 | assertEquals(map.get("height"), new ObjectExpectationInput(170)); 117 | assertEquals(map.get("drinks"), new ObjectExpectationInput(drinksList)); 118 | assertEquals(map.get("is_cool"), new ObjectExpectationInput(true)); 119 | assertEquals(map.get("driver"), new ObjectExpectationInput(driverMap)); 120 | 121 | // and predicate from field "id" is correct 122 | PredicateExpectationInput predicateExpectationInput = (PredicateExpectationInput) map.get("id"); 123 | Predicate predicate = predicateExpectationInput.predicate(); 124 | assertFalse(predicate.test(-10)); 125 | assertFalse(predicate.test(-1)); 126 | assertFalse(predicate.test(0)); 127 | assertTrue(predicate.test(1)); 128 | assertTrue(predicate.test(10)); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /jazon-junit/src/test/java/com/zendesk/jazon/junit/JunitSpecificMatcherTest.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.junit; 2 | 3 | import com.zendesk.jazon.MatchResult; 4 | import com.zendesk.jazon.MatcherFactory; 5 | import com.zendesk.jazon.actual.factory.GsonActualFactory; 6 | import com.zendesk.jazon.expectation.translator.DefaultTranslators; 7 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators; 8 | import com.zendesk.jazon.expectation.translator.JunitTranslators; 9 | import com.zendesk.jazon.expectation.translator.TranslatorFacade; 10 | import com.zendesk.jazon.mismatch.impl.PredicateExecutionFailedMismatch; 11 | import com.zendesk.jazon.mismatch.impl.PrimitiveValueMismatch; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.Stream; 17 | 18 | import static com.zendesk.jazon.expectation.Expectations.anyNumberOf; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertFalse; 21 | import static org.junit.jupiter.api.Assertions.assertTrue; 22 | 23 | class JunitSpecificMatcherTest { 24 | private static final MatcherFactory matcherFactory = new MatcherFactory( 25 | new TranslatorFacade( 26 | Stream.of( 27 | DefaultTranslators.translators(), 28 | JazonTypesTranslators.translators(), 29 | JunitTranslators.translators() 30 | ) 31 | .flatMap(List::stream) 32 | .collect(Collectors.toList()) 33 | ), 34 | new GsonActualFactory() 35 | ); 36 | 37 | @Test 38 | void testRegex() { 39 | // given 40 | JazonMap expected = new JazonMap() 41 | .with("first", (String s) -> s.matches("bl.*")) 42 | .with("second", s -> ((String) s).matches("bl.*")) 43 | .with("third", "red"); 44 | String actualJson = "{" + 45 | " \"first\": \"blue\"," + 46 | " \"second\": \"black\"," + 47 | " \"third\": \"red\"" + 48 | "}"; 49 | 50 | // when 51 | MatchResult matchResult = match(expected, actualJson); 52 | 53 | // then 54 | assertTrue(matchResult.ok()); 55 | } 56 | 57 | @Test 58 | void testRegexTypeMismatch() { 59 | // given 60 | JazonMap expected = new JazonMap() 61 | .with("first", (String s) -> s.matches("bl.*")); 62 | String actualJson = "{" + 63 | " \"first\": 55" + 64 | "}"; 65 | 66 | // when 67 | MatchResult matchResult = match(expected, actualJson); 68 | 69 | // then 70 | assertFalse(matchResult.ok()); 71 | assertEquals(matchResult.mismatch().expectationMismatch().getClass(), PredicateExecutionFailedMismatch.class); 72 | assertTrue( 73 | matchResult.mismatch().expectationMismatch().message().startsWith( 74 | "Exception occurred on predicate evaluation: \n\njava.lang.ClassCastException" 75 | ) 76 | ); 77 | assertEquals(matchResult.mismatch().path(), "$.first"); 78 | } 79 | 80 | @Test 81 | void testPredicatedWithDeeplyNestedException() { 82 | // given 83 | JazonMap expected = new JazonMap() 84 | .with("first", this::complexOperation); 85 | String actualJson = "{" + 86 | " \"first\": 55" + 87 | "}"; 88 | 89 | // when 90 | MatchResult matchResult = match(expected, actualJson); 91 | 92 | // then 93 | assertFalse(matchResult.ok()); 94 | assertEquals(matchResult.mismatch().expectationMismatch().getClass(), PredicateExecutionFailedMismatch.class); 95 | assertTrue( 96 | matchResult.mismatch().expectationMismatch().message().startsWith( 97 | "Exception occurred on predicate evaluation: \n\n" + 98 | "java.lang.RuntimeException: an intentional exception for value 55" 99 | ) 100 | ); 101 | assertEquals(matchResult.mismatch().path(), "$.first"); 102 | } 103 | 104 | @Test 105 | void testAnyNumberOfExpectationSuccess() { 106 | // given 107 | JazonMap expected = new JazonMap() 108 | .with("animals", anyNumberOf("cat")); 109 | String actualJson = "{" + 110 | " \"animals\": [\"cat\", \"cat\", \"cat\", \"cat\", \"cat\", \"cat\", \"cat\"]" + 111 | "}"; 112 | 113 | // when 114 | MatchResult matchResult = match(expected, actualJson); 115 | 116 | // then 117 | assertTrue(matchResult.ok()); 118 | } 119 | 120 | @Test 121 | void testAnyNumberOfExpectationFailure() { 122 | // given 123 | JazonMap expected = new JazonMap() 124 | .with("animals", anyNumberOf("cat")); 125 | String actualJson = "{" + 126 | " \"animals\": [\"cat\", \"cat\", \"dog\", \"cat\", \"cat\", \"cat\"]" + 127 | "}"; 128 | 129 | // when 130 | MatchResult matchResult = match(expected, actualJson); 131 | 132 | // then 133 | assertFalse(matchResult.ok()); 134 | assertEquals(matchResult.mismatch().expectationMismatch().getClass(), PrimitiveValueMismatch.class); 135 | assertEquals(matchResult.mismatch().path(), "$.animals.2"); 136 | } 137 | 138 | private boolean complexOperation(Integer number) { 139 | return failingOperation(number); 140 | } 141 | 142 | private boolean failingOperation(int number) { 143 | throw new RuntimeException("an intentional exception for value " + number); 144 | } 145 | 146 | private MatchResult match(JazonMap expected, String actualJson) { 147 | return matcherFactory.matcher() 148 | .expected(expected.map()) 149 | .actual(actualJson) 150 | .match(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /jazon-spock/README.md: -------------------------------------------------------------------------------- 1 | # Jazon Spock 2 | A library for test assertions on JSON payloads - for Spock framework. 3 | 4 | ## Quickstart 5 | 6 | #### Example 1: Simple exact-match 7 | 8 | ```groovy 9 | def "simple assertion passes"() { 10 | when: 11 | def response = ''' 12 | { 13 | "firstname": "Steve", 14 | "lastname": "Jobs" 15 | } 16 | ''' 17 | 18 | then: 19 | jazon(response).matches([firstname: 'Steve', lastname: 'Jobs']) 20 | } 21 | ``` 22 | 23 | #### Example 2: Unordered array 24 | 25 | This assertion passes even though the items in the array are in different order. 26 | 27 | ```groovy 28 | def "unordered array assertion passes"() { 29 | when: 30 | def response = ''' 31 | { 32 | "id": 95478, 33 | "name": "Coca Cola", 34 | "tags": ["sprite", "pepsi", "7up", "fanta", "dr pepper"] 35 | } 36 | ''' 37 | 38 | then: 39 | jazon(response).matches([ 40 | id: 95478, 41 | name: 'Coca Cola', 42 | tags: ['pepsi', 'dr pepper', 'sprite', 'fanta', '7up'] as Set 43 | ]) 44 | } 45 | ``` 46 | 47 | #### Example 3: Custom assertions 48 | 49 | If you need, instead of exact-matching, you can define custom assertions using Closures. 50 | Here for example, we used custom assertions: 51 | * to check that a number is in given range - `{ it >= 0 }` 52 | * to check that a field matches a regex - `{ it ==~ /\d+\.\d\d/ }` 53 | * to check that a field just exists, no matter of its value - `{ it != null }` 54 | 55 | ```groovy 56 | def "custom assertions"() { 57 | when: 58 | def response = ''' 59 | { 60 | "id": 95478, 61 | "name": "Coca Cola", 62 | "value": "133.30", 63 | "updated_at": "1990-06-19T12:19:10Z" 64 | } 65 | ''' 66 | 67 | then: 68 | jazon(response).matches([ 69 | id: { it >= 0 }, 70 | name: 'Coca Cola', 71 | value: { it ==~ /\d+\.\d\d/ }, 72 | updated_at: { it != null } 73 | ]) 74 | } 75 | ``` 76 | 77 | #### Example 4: Utils extraction 78 | 79 | To avoid code duplication, you can extract your common wildcard-assertions to constants. 80 | 81 | ```groovy 82 | def "utils extraction"() { 83 | when: 84 | def response = ''' 85 | { 86 | "id": 95478, 87 | "name": "Coca Cola", 88 | "value": "133.30", 89 | "updated_at": "1990-06-19T12:19:10Z" 90 | } 91 | ''' 92 | 93 | then: 94 | jazon(response).matches([ 95 | id: ANY_ID, // a constant 96 | name: 'Coca Cola', 97 | value: '133.30', 98 | updated_at: ANY_ISO_DATETIME // a constant 99 | ]) 100 | } 101 | ``` 102 | 103 | #### Example 5: Utils extraction to domain objects 104 | 105 | To avoid code duplication even more, you can extract the parts of JSON. This will also 106 | make your tests more readable. 107 | Here in the examples we extract `deal()` - a business object from sales domain. 108 | 109 | ```groovy 110 | jazon(response).matches deal(name: 'Coca Cola', value: '10.00') 111 | ``` 112 | 113 | ```groovy 114 | jazon(response).matches([ 115 | deal(name: 'Coca Cola', value: '10.00'), 116 | deal(name: 'Pepsi', value: '9.00'), 117 | deal(name: 'Fanta', value: '10.00'), 118 | deal(name: 'Sprite', value: '10.00'), 119 | deal(name: 'Dr Pepper', value: '12.00') 120 | ]) 121 | ``` 122 | 123 | ```groovy 124 | private Map deal(Map kwargs) { 125 | Map defaults = [ 126 | id: ANY_ID, 127 | updated_at: ANY_ISO_DATETIME 128 | ] 129 | defaults + kwargs 130 | } 131 | ``` 132 | 133 | ## Copyright and license 134 | Copyright 2019 Zendesk, Inc. 135 | 136 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 137 | You may obtain a copy of the License at 138 | http://www.apache.org/licenses/LICENSE-2.0 139 | 140 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 141 | 142 | -------------------------------------------------------------------------------- /jazon-spock/build.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | name = 'Jazon Spock Adapter' 3 | description = 'A library for test assertions on JSON payloads - for Spock framework.' 4 | } 5 | 6 | apply from: '../gradle/publishing.gradle' 7 | 8 | dependencies { 9 | compile project(':jazon-core') 10 | compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.12' 11 | 12 | testCompile group: 'org.spockframework', name: 'spock-core', version: '1.2-groovy-2.4' 13 | } 14 | -------------------------------------------------------------------------------- /jazon-spock/src/main/groovy/com/zendesk/jazon/spock/JazonSpockAdapter.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.spock 2 | 3 | import com.zendesk.jazon.MatchResult 4 | import com.zendesk.jazon.MatcherFactory 5 | import com.zendesk.jazon.actual.factory.GsonActualFactory 6 | import com.zendesk.jazon.expectation.translator.DefaultTranslators 7 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators 8 | import com.zendesk.jazon.expectation.translator.SpockTranslators 9 | import com.zendesk.jazon.expectation.translator.TranslatorFacade 10 | import com.zendesk.jazon.expectation.translator.TranslatorMapping 11 | 12 | class JazonSpockAdapter { 13 | private static final MatcherFactory MATCHER_FACTORY = new MatcherFactory( 14 | new TranslatorFacade(translators()), 15 | new GsonActualFactory() 16 | ) 17 | private final String json 18 | 19 | private JazonSpockAdapter(String json) { 20 | this.json = json 21 | } 22 | 23 | static JazonSpockAdapter jazon(String json) { 24 | return new JazonSpockAdapter(json) 25 | } 26 | 27 | boolean matches(Map jsonAsMap) { 28 | return match(jsonAsMap) 29 | } 30 | 31 | boolean matches(List jsonAsList) { 32 | return match(jsonAsList) 33 | } 34 | 35 | private boolean match(Object expected) { 36 | MatchResult matchResult = MATCHER_FACTORY.matcher() 37 | .expected(expected) 38 | .actual(json) 39 | .match() 40 | if (matchResult.ok()) { 41 | return true 42 | } 43 | throw new AssertionError(errorMessage(matchResult)) 44 | } 45 | 46 | private static GString errorMessage(MatchResult matchResult) { 47 | "\n-----------------------------------\nJSON MISMATCH:\n${matchResult.message()}\n-----------------------------------\n" 48 | } 49 | 50 | private static List translators() { 51 | DefaultTranslators.translators() + JazonTypesTranslators.translators() + SpockTranslators.translators() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /jazon-spock/src/main/java/com/zendesk/jazon/expectation/translator/SpockTranslators.java: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.expectation.translator; 2 | 3 | import com.zendesk.jazon.actual.ActualJsonString; 4 | import com.zendesk.jazon.expectation.JsonExpectation; 5 | import com.zendesk.jazon.expectation.impl.PredicateExpectation; 6 | import com.zendesk.jazon.expectation.impl.PrimitiveValueExpectation; 7 | import groovy.lang.Closure; 8 | import groovy.lang.GString; 9 | 10 | import java.util.List; 11 | 12 | import static java.util.Arrays.asList; 13 | 14 | public class SpockTranslators { 15 | public static List> translators() { 16 | return asList( 17 | new TranslatorMapping<>(GString.class, new GStringTranslator()), 18 | new TranslatorMapping<>(Closure.class, new ClosureTranslator()) 19 | ); 20 | } 21 | 22 | private static class GStringTranslator implements Translator { 23 | @Override 24 | public JsonExpectation jsonExpectation(GString gstring, TranslatorFacade translator) { 25 | return new PrimitiveValueExpectation<>(new ActualJsonString(gstring.toString())); 26 | } 27 | } 28 | 29 | @SuppressWarnings({"rawtypes", "unchecked"}) 30 | private static class ClosureTranslator implements Translator { 31 | @Override 32 | public JsonExpectation jsonExpectation(Closure object, TranslatorFacade translator) { 33 | Closure booleanClosure = (Closure) object; 34 | return new PredicateExpectation(booleanClosure::call); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /jazon-spock/src/test/groovy/com/zendesk/jazon/ExampleSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon 2 | 3 | import spock.lang.FailsWith 4 | import spock.lang.Specification 5 | 6 | import static com.zendesk.jazon.spock.JazonSpockAdapter.jazon 7 | 8 | class ExampleSpec extends Specification { 9 | 10 | def "smoke test"() { 11 | expect: 12 | jazon('{"shark": "white"}').matches([shark: 'white']) 13 | } 14 | 15 | @FailsWith(AssertionError) 16 | def "failure format test"() { 17 | expect: 18 | jazon('{"shark": "white", "raccoon": "red"}').matches([shark: 'white']) 19 | } 20 | 21 | def "predicate expectation test"() { 22 | expect: 23 | jazon('{"shark": "white"}').matches([ 24 | shark: { it.startsWith('whi') } 25 | ]) 26 | } 27 | 28 | def "array can be root JSON: success"() { 29 | expect: 30 | jazon('["platypus", "narwhal"]').matches(['platypus', 'narwhal']) 31 | } 32 | 33 | @FailsWith(AssertionError) 34 | def "array can be root JSON: fails"() { 35 | expect: 36 | jazon('["platypus", "narwhal"]').matches(['platypus', 'lynx']) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /jazon-spock/src/test/groovy/com/zendesk/jazon/MatcherForGroovySpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon 2 | 3 | import com.zendesk.jazon.actual.factory.GsonActualFactory 4 | import com.zendesk.jazon.expectation.translator.DefaultTranslators 5 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators 6 | import com.zendesk.jazon.expectation.translator.SpockTranslators 7 | import com.zendesk.jazon.expectation.translator.TranslatorFacade 8 | import com.zendesk.jazon.mismatch.impl.PredicateMismatch 9 | import spock.lang.Specification 10 | 11 | import static com.zendesk.jazon.expectation.Expectations.anyNumberOf 12 | import static groovy.json.JsonOutput.toJson 13 | 14 | class MatcherForGroovySpec extends Specification { 15 | 16 | MatcherFactory matcherFactory = new MatcherFactory( 17 | new TranslatorFacade( 18 | DefaultTranslators.translators() + JazonTypesTranslators.translators() + SpockTranslators.translators() 19 | ), 20 | new GsonActualFactory() 21 | ) 22 | 23 | def "predicate expectation: succeeds"() { 24 | given: 25 | Closure closure = { it ==~ "dig.*" } 26 | 27 | when: 28 | def result = matcherFactory.matcher() 29 | .expected([a: closure]) 30 | .actual(toJson([a: stringToMatch])) 31 | .match() 32 | 33 | then: 34 | result.ok() 35 | 36 | where: 37 | stringToMatch << [ 38 | 'dig', 39 | 'digger', 40 | 'digging', 41 | 'digs', 42 | ] 43 | } 44 | 45 | def "predicate expectation: fails"() { 46 | given: 47 | Closure closure = { it ==~ "dig.*" } 48 | 49 | when: 50 | def result = matcherFactory.matcher() 51 | .expected([a: closure]) 52 | .actual(toJson([a: stringToMatch])) 53 | .match() 54 | 55 | then: 56 | !result.ok() 57 | result.mismatch().expectationMismatch() == PredicateMismatch.INSTANCE 58 | result.mismatch().path() == '$.a' 59 | 60 | where: 61 | stringToMatch << [ 62 | 'dog', 63 | 'di', 64 | 'do', 65 | 'dagger', 66 | 'refrigerator', 67 | ] 68 | } 69 | 70 | def "predicate expectation can be root"() { 71 | when: 72 | def result = matcherFactory.matcher() 73 | .expected({ it ==~ 'dig.*' }) 74 | .actual('refrigerator') 75 | .match() 76 | 77 | then: 78 | !result.ok() 79 | result.mismatch().expectationMismatch() == PredicateMismatch.INSTANCE 80 | result.mismatch().path() == '$' 81 | } 82 | 83 | def "Groovy's GString works well"() { 84 | given: 85 | def someVariable = 123 86 | def gstring = "this is interpolated string with $someVariable" 87 | 88 | when: 89 | def result = matcherFactory.matcher() 90 | .expected([a: gstring]) 91 | .actual(toJson([a: 'this is interpolated string with 123'])) 92 | .match() 93 | 94 | then: 95 | result.ok() 96 | } 97 | 98 | def "predicate expectation for array: fails"() { 99 | when: 100 | def result = matcherFactory.matcher() 101 | .expected([a: predicate]) 102 | .actual(toJson([a: ['chips', 'ketchup', 'fish']])) 103 | .match() 104 | 105 | then: 106 | !result.ok() 107 | result.mismatch().expectationMismatch() == PredicateMismatch.INSTANCE 108 | result.mismatch().path() == '$.a' 109 | 110 | where: 111 | predicate << [ 112 | { it.size() == 9 }, 113 | { it[1] == 'fish' }, 114 | ] 115 | } 116 | 117 | def "predicate expectation for array succeeds"() { 118 | when: 119 | def result = matcherFactory.matcher() 120 | .expected([a: predicate]) 121 | .actual(toJson([a: ['chips', 'ketchup', 'fish']])) 122 | .match() 123 | 124 | then: 125 | result.ok() 126 | 127 | where: 128 | predicate << [ 129 | { it.size() == 3 }, 130 | { it[1] == 'ketchup' }, 131 | ] 132 | } 133 | 134 | def "predicate expectation for object: fails"() { 135 | when: 136 | def result = matcherFactory.matcher() 137 | .expected([a: predicate]) 138 | .actual(toJson([a: [name: 'tomato', color: 'red']])) 139 | .match() 140 | 141 | then: 142 | !result.ok() 143 | result.mismatch().expectationMismatch() == PredicateMismatch.INSTANCE 144 | result.mismatch().path() == '$.a' 145 | 146 | where: 147 | predicate << [ 148 | { it.size() == 17 }, 149 | { it.name == 'cucumber' }, 150 | ] 151 | } 152 | 153 | def "predicate expectation for object succeeds"() { 154 | when: 155 | def result = matcherFactory.matcher() 156 | .expected([a: predicate]) 157 | .actual(toJson([a: [name: 'tomato', color: 'red']])) 158 | .match() 159 | 160 | then: 161 | result.ok() 162 | 163 | where: 164 | predicate << [ 165 | { it.name == 'tomato' }, 166 | { it.color == 'red' }, 167 | ] 168 | } 169 | 170 | def "array each element expectation works correctly with lambda-style expectation"() { 171 | when: 172 | def result = matcherFactory.matcher() 173 | .expected([a: anyNumberOf {it -> it > 5}]) 174 | .actual(toJson([a: [6, 7, 8, 9, 0]])) 175 | .match() 176 | 177 | then: 178 | !result.ok() 179 | result.mismatch().expectationMismatch() == PredicateMismatch.INSTANCE 180 | result.mismatch().path() == '$.a.4' 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /jazon-spock/src/test/groovy/com/zendesk/jazon/MessagesForGroovySpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon 2 | 3 | 4 | import com.zendesk.jazon.actual.factory.GsonActualFactory 5 | import com.zendesk.jazon.expectation.translator.DefaultTranslators 6 | import com.zendesk.jazon.expectation.translator.JazonTypesTranslators 7 | import com.zendesk.jazon.expectation.translator.SpockTranslators 8 | import com.zendesk.jazon.expectation.translator.TranslatorFacade 9 | import spock.lang.Specification 10 | 11 | import static groovy.json.JsonOutput.toJson 12 | 13 | class MessagesForGroovySpec extends Specification { 14 | 15 | MatcherFactory matcherFactory = new MatcherFactory( 16 | new TranslatorFacade( 17 | DefaultTranslators.translators() + JazonTypesTranslators.translators() + SpockTranslators.translators() 18 | ), 19 | new GsonActualFactory() 20 | ) 21 | 22 | def "predicate expectation: fails"() { 23 | given: 24 | Closure closure = { it ==~ "dig.*" } 25 | 26 | when: 27 | def result = matcherFactory.matcher() 28 | .expected([a: closure]) 29 | .actual(toJson([a: 'rosemary'])) 30 | .match() 31 | 32 | then: 33 | result.message() == 'Mismatch at path: $.a\nCustom predicate does not match the value.' 34 | println result.message() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /jazon-spock/src/test/groovy/com/zendesk/jazon/spock/JazonSpockAdapterSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.zendesk.jazon.spock 2 | 3 | 4 | import spock.lang.Specification 5 | 6 | import static com.zendesk.jazon.spock.JazonSpockAdapter.jazon 7 | 8 | class JazonSpockAdapterSpec extends Specification { 9 | 10 | def 'formats the error message well'() { 11 | when: 12 | jazon('["platypus", "narwhal"]') 13 | .matches(['platypus', 'penguin']) 14 | 15 | then: 16 | AssertionError assertionError = thrown() 17 | assertionError.toString() == EXPECTED_ERROR_MESSAGE 18 | } 19 | 20 | def 'smoke test for all types at once'() { 21 | expect: 22 | jazon(JSON_WITH_ALL_TYPES).matches([ 23 | nested_object: [ 24 | some_number: 123 25 | ], 26 | nested_array: ['red', 'green', 'blue'], 27 | some_boolean: true, 28 | a_null: null, 29 | some_string: 'whatever', 30 | some_integer: 123, 31 | some_long: 123456789012345678, 32 | some_double: 99.77 33 | ]) 34 | } 35 | 36 | private static final String EXPECTED_ERROR_MESSAGE = '''java.lang.AssertionError: 37 | ----------------------------------- 38 | JSON MISMATCH: 39 | Mismatch at path: $.1 40 | Expected: "penguin" 41 | Actual: "narwhal" 42 | ----------------------------------- 43 | ''' 44 | private static final String JSON_WITH_ALL_TYPES = ''' 45 | { 46 | "nested_object": { 47 | "some_number": 123 48 | }, 49 | "nested_array": ["red", "green", "blue"], 50 | "some_boolean": true, 51 | "a_null": null, 52 | "some_string": "whatever", 53 | "some_integer": 123, 54 | "some_long": 123456789012345678, 55 | "some_double": 99.77 56 | } 57 | ''' 58 | } 59 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jazon' 2 | include 'jazon-core' 3 | include 'jazon-spock' 4 | include 'jazon-junit' 5 | include 'examples' 6 | 7 | --------------------------------------------------------------------------------