├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── commonMain └── kotlin │ └── com │ └── xurxodev │ └── integrationtesting │ ├── Either.kt │ ├── TodoApiClient.kt │ ├── error │ └── ApiError.kt │ └── model │ └── Task.kt ├── commonTest ├── kotlin │ └── com │ │ └── xurxodev │ │ └── integrationtesting │ │ ├── TodoApiClientShould.kt │ │ └── common │ │ ├── api │ │ ├── MockResponse.kt │ │ └── TodoApiMockEngine.kt │ │ ├── coroutines │ │ └── runTest.kt │ │ └── responses │ │ ├── addTaskRequest.kt │ │ ├── addTaskResponse.kt │ │ ├── getTaskByIdResponse.kt │ │ ├── getTasksResponse.kt │ │ ├── updateTaskRequest.kt │ │ └── updateTaskResponse.kt └── resources │ └── getTasksResponse.json ├── iosTest └── kotlin │ └── com │ └── xurxodev │ └── integrationtesting │ └── common │ └── coroutines │ └── runTest.kt └── jvmTest └── kotlin └── com └── xurxodev └── integrationtesting └── common └── coroutines └── runTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode10.1 3 | 4 | script: 5 | - ./gradlew build -------------------------------------------------------------------------------- /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 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ![integration-testing-kotlin-multiplatform](https://user-images.githubusercontent.com/5593590/62697749-b78da300-b9db-11e9-92e4-329ce3d39bd9.png) 3 | 4 | [![Build Status](https://travis-ci.org/xurxodev/integration-testing-kotlin-multiplatform-kata.svg?branch=master)](https://travis-ci.org/xurxodev/integration-testing-kotlin-multiplatform-kata) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin#examples-back-) 5 | 6 | ![](http://xurxodev.com/content/images/2019/02/Kotlin-multiplatform-library.png) 7 | 8 | This kata is a Kotlin multiplatform version of the kata [KataTODOApiClientKotlin][KataTODOApiClientKotlin] of [Karumi][karumi]. 9 | 10 | - We are here to practice integration testing using HTTP stubbing. 11 | - We are going to use [KtorClientMock][ktorclientmock] to return stub responses. 12 | - We are going to use [KotlinTest][kotlintest] to write tests and to perform assertions. 13 | - We are going to practice pair programming. 14 | 15 | --- 16 | 17 | ## Getting started 18 | 19 | This repository contains an kotlin multiplatform library with an API client to interact with the [JSONPlaceholder service](http://jsonplaceholder.typicode.com). 20 | 21 | This API Client is based on one class with name ``TodoApiClient`` containing some methods to interact with the API. Using this class we can get all the tasks, get a task using the task id, add a new task, update a task or delete an already created task. 22 | 23 | The API client has been implemented using a multiplatform networking framework named [Ktor][ktor]. Review the project documentation if needed. 24 | 25 | ## Tasks 26 | 27 | Your task as a multiplatform Kotlin Developer is to **write all the integration tests** needed to check if the API Client is working as expected. 28 | 29 | **This repository is ready to build the application, pass the checkstyle using ktlint and your tests in Travis-CI environments.** 30 | 31 | My recommendation for this exercise is: 32 | 33 | * Before starting 34 | 1. Fork this repository. 35 | 2. Checkout `integration-testing-kotlin-multiplatform-kata` branch. 36 | 3. Execute the repository playground and make yourself familiar with the code. 37 | 4. Execute `TodoApiClientShould` and watch the only test it contains pass. 38 | 39 | * To help you get started, these are some tests already written at `TodoApiClientShould ` class. Review it carefully before to start writing your own tests. Here you have the description of some tests you can write to start working on this Kata: 40 | 1. Test that the ``Accept`` headers are sent. 41 | 2. Test that the list of ``Task`` instances obtained invoking the getter method of the property ``allTasks`` contains the expected values. 42 | 3. Test that the request is sent to the correct path using the correct HTTP method. 43 | 4. Test that adding a task the body sent to the server is the correct one. 44 | 45 | ## Considerations 46 | 47 | * If you get stuck, `master` branch contains all the tests already solved. 48 | 49 | * You will find some utilities to help you test the APIClient easily in: 50 | ``TodoApiMockEngine`` and the common/responses directory in commonTest source set. 51 | 52 | ## Extra Tasks 53 | 54 | If you've covered all the application functionality using integration tests you can continue with some extra tasks: 55 | 56 | * Create your own API client to consume one of the services described in this web: [http://jsonplaceholder.typicode.com/][jsonplaceholder] 57 | 58 | --- 59 | 60 | ## Documentation 61 | 62 | There are some links which can be useful to finish these tasks: 63 | 64 | * [KtorMockClient official documentation][ktorclientmock] 65 | * [Kotlin Test documentation][kotlintest] 66 | * [Ktor documentation][ktor] 67 | * [How to create a REST API client and its integration tests in Kotlin Multiplatform][how-to-create-a-rest-api-client-and-its-integration-tests-in-kotlin-multiplatform] (English version) 68 | * [Cómo crear un cliente API REST y sus tests de integración en Kotlin Multiplatform][cliente-api-rest-y-test-de-integracion-en-kotlin-multiplatform] (Spanish version) 69 | 70 | # License 71 | 72 | Copyright 2019 Jorge Sánchez Fernández 73 | 74 | Licensed under the Apache License, Version 2.0 (the "License"); 75 | you may not use this file except in compliance with the License. 76 | You may obtain a copy of the License at 77 | 78 | http://www.apache.org/licenses/LICENSE-2.0 79 | 80 | Unless required by applicable law or agreed to in writing, software 81 | distributed under the License is distributed on an "AS IS" BASIS, 82 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 83 | See the License for the specific language governing permissions and 84 | limitations under the License. 85 | 86 | [xurxodevlogo]: http://xurxodev.com/content/images/2017/04/xurxodev-readme.png 87 | [ktorclientmock]: https://ktor.io/clients/http-client/testing.html 88 | [kotlintest]: https://kotlinlang.org/api/latest/kotlin.test/index.html 89 | [jsonplaceholder]: http://jsonplaceholder.typicode.com/ 90 | [cliente-api-rest-y-test-de-integracion-en-kotlin-multiplatform]: http://xurxodev.com/cliente-api-rest-y-test-de-integracion-en-kotlin-multiplatform 91 | [how-to-create-a-rest-api-client-and-its-integration-tests-in-kotlin-multiplatform]: https://medium.com/@xurxodev/how-to-create-a-rest-api-client-and-its-integration-tests-in-kotlin-multiplatform-d76c9a1be348 92 | [ktor]: https://ktor.io/ 93 | [KataTODOApiClientKotlin]: https://github.com/Karumi/KataTODOApiClientKotlin 94 | [karumi]: https://github.com/Karumi 95 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'kotlin-multiplatform' version '1.3.21' 3 | id 'kotlinx-serialization' version '1.3.21' 4 | } 5 | 6 | group 'com.xurxodev' 7 | version '0.0.1' 8 | 9 | repositories { 10 | google() 11 | jcenter() 12 | maven { url "https://kotlin.bintray.com/kotlinx" } 13 | } 14 | 15 | kotlin { 16 | def serialization_version = "0.10.0" 17 | def ktor_version = "1.1.2" 18 | def coroutines_version = "1.1.1" 19 | 20 | targets { 21 | fromPreset(presets.jvm, 'jvm') 22 | 23 | final def iOSTarget = System.getenv('SDK_NAME')?.startsWith("iphoneos") \ 24 | ? presets.iosArm64 : presets.iosX64 25 | 26 | fromPreset(iOSTarget, 'iOS') { 27 | compilations.main.outputKinds('FRAMEWORK') 28 | } 29 | } 30 | sourceSets { 31 | commonMain { 32 | dependencies { 33 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-common' 34 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version" 35 | 36 | implementation "io.ktor:ktor-client-core:$ktor_version" 37 | implementation "io.ktor:ktor-client-json:$ktor_version" 38 | } 39 | } 40 | commonTest { 41 | dependencies { 42 | implementation 'org.jetbrains.kotlin:kotlin-test-common' 43 | implementation 'org.jetbrains.kotlin:kotlin-test-annotations-common' 44 | 45 | api "io.ktor:ktor-client-mock:$ktor_version" 46 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutines_version" 47 | } 48 | } 49 | jvmMain { 50 | dependencies { 51 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' 52 | 53 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" 54 | 55 | implementation "io.ktor:ktor-client-core-jvm:$ktor_version" 56 | implementation "io.ktor:ktor-client-json-jvm:$ktor_version" 57 | implementation "io.ktor:ktor-client-okhttp:$ktor_version" 58 | } 59 | } 60 | jvmTest { 61 | dependencies { 62 | implementation 'junit:junit:4.12' 63 | implementation 'org.jetbrains.kotlin:kotlin-test' 64 | implementation 'org.jetbrains.kotlin:kotlin-test-junit' 65 | 66 | api "io.ktor:ktor-client-mock-jvm:$ktor_version" 67 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" 68 | } 69 | } 70 | iOSMain { 71 | dependencies { 72 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version" 73 | 74 | implementation "io.ktor:ktor-client-ios:$ktor_version" 75 | implementation "io.ktor:ktor-client-core-native:$ktor_version" 76 | implementation "io.ktor:ktor-client-json-native:$ktor_version" 77 | } 78 | 79 | } 80 | iOSTest { 81 | dependencies { 82 | api "io.ktor:ktor-client-mock-native:$ktor_version" 83 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core-native:$coroutines_version" 84 | } 85 | } 86 | } 87 | } 88 | 89 | task iosTest { 90 | doLast { 91 | def binary = kotlin.targets.iOS.compilations.test.getBinary('EXECUTABLE', 'DEBUG') 92 | exec { 93 | commandLine 'xcrun', 'simctl', 'spawn', "iPhone XR", binary.absolutePath 94 | } 95 | } 96 | } 97 | tasks.check.dependsOn iosTest 98 | 99 | tasks.withType(Test) { 100 | testLogging { 101 | exceptionFormat "full" 102 | events "passed", "failed" 103 | showStandardStreams true 104 | } 105 | } 106 | 107 | configurations { 108 | ktlint 109 | } 110 | 111 | dependencies { 112 | ktlint 'com.github.shyiko:ktlint:0.29.0' 113 | } 114 | 115 | task ktlint(type: JavaExec) { 116 | main = "com.github.shyiko.ktlint.Main" 117 | classpath = configurations.ktlint 118 | args "src/**/*.kt" 119 | } 120 | 121 | check.dependsOn ktlint 122 | 123 | task ktlintFormat(type: JavaExec) { 124 | main = "com.github.shyiko.ktlint.Main" 125 | classpath = configurations.ktlint 126 | args "-F", "src/**/*.kt" 127 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xurxodev/integration-testing-kotlin-multiplatform-kata/5b485f4afcc70b482d2a1d00d307180e30077ed5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Feb 17 14:30:14 CET 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | resolutionStrategy { 3 | eachPlugin { 4 | if (requested.id.id == "kotlin-multiplatform") { 5 | useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") 6 | } 7 | if (requested.id.id == "kotlinx-serialization") { 8 | useModule("org.jetbrains.kotlin:kotlin-serialization:${requested.version}") 9 | } 10 | } 11 | } 12 | } 13 | 14 | enableFeaturePreview('GRADLE_METADATA') 15 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/xurxodev/integrationtesting/Either.kt: -------------------------------------------------------------------------------- 1 | package todoapiclient 2 | 3 | sealed class Either { 4 | data class Left(val value: L) : Either() 5 | data class Right(val value: R) : Either() 6 | 7 | val isRight get() = this is Right 8 | val isLeft get() = this is Left 9 | 10 | fun left(a: L) = Left(a) 11 | fun right(b: R) = Right(b) 12 | } 13 | 14 | fun Either.fold(left: (L) -> T, right: (R) -> T): T = 15 | when (this) { 16 | is Either.Left -> left(value) 17 | is Either.Right -> right(value) 18 | } 19 | 20 | fun Either.flatMap(f: (R) -> Either): Either = 21 | fold({ this as Either.Left }, f) 22 | 23 | fun Either.map(f: (R) -> T): Either = 24 | flatMap { Either.Right(f(it)) } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/xurxodev/integrationtesting/TodoApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting 2 | 3 | import com.xurxodev.integrationtesting.error.ApiError 4 | import com.xurxodev.integrationtesting.error.UnknownError 5 | import com.xurxodev.integrationtesting.error.ItemNotFoundError 6 | import com.xurxodev.integrationtesting.error.NetworkError 7 | import com.xurxodev.integrationtesting.model.Task 8 | import io.ktor.client.HttpClient 9 | import io.ktor.client.engine.HttpClientEngine 10 | import io.ktor.client.features.BadResponseStatusException 11 | import io.ktor.client.features.json.JsonFeature 12 | import io.ktor.client.features.json.serializer.KotlinxSerializer 13 | import io.ktor.client.request.delete 14 | import io.ktor.client.request.get 15 | import io.ktor.client.request.post 16 | import io.ktor.client.request.put 17 | import io.ktor.http.ContentType 18 | import io.ktor.http.contentType 19 | import kotlinx.serialization.json.Json 20 | import kotlinx.serialization.list 21 | import todoapiclient.Either 22 | 23 | class TodoApiClient constructor( 24 | httpClientEngine: HttpClientEngine? = null 25 | ) { 26 | 27 | companion object { 28 | const val BASE_ENDPOINT = "http://jsonplaceholder.typicode.com" 29 | } 30 | 31 | private val client: HttpClient = HttpClient(httpClientEngine!!) { 32 | install(JsonFeature) { 33 | serializer = KotlinxSerializer().apply { 34 | // It's necessary register the serializer because: 35 | // Obtaining serializer from KClass is not available on native 36 | // due to the lack of reflection 37 | register(Task.serializer()) 38 | } 39 | } 40 | } 41 | 42 | suspend fun getAllTasks(): Either> = try { 43 | val tasksJson = client.get("$BASE_ENDPOINT/todos") 44 | 45 | // JsonFeature does not working currently with root-level array 46 | // https://github.com/Kotlin/kotlinx.serialization/issues/179 47 | val tasks = Json.nonstrict.parse(Task.serializer().list, tasksJson) 48 | 49 | Either.Right(tasks) 50 | } catch (e: Exception) { 51 | handleError(e) 52 | } 53 | 54 | suspend fun getTasksById(id: String): Either = try { 55 | val task = client.get("$BASE_ENDPOINT/todos/$id") 56 | 57 | Either.Right(task) 58 | } catch (e: Exception) { 59 | handleError(e) 60 | } 61 | 62 | suspend fun addTask(task: Task): Either = try { 63 | val taskResponse = client.post("$BASE_ENDPOINT/todos") { 64 | contentType(ContentType.Application.Json) 65 | body = task 66 | } 67 | 68 | Either.Right(taskResponse) 69 | } catch (e: Exception) { 70 | handleError(e) 71 | } 72 | 73 | suspend fun updateTask(task: Task): Either = try { 74 | val taskResponse = client.put("$BASE_ENDPOINT/todos/${task.id}") { 75 | contentType(ContentType.Application.Json) 76 | body = task 77 | } 78 | 79 | Either.Right(taskResponse) 80 | } catch (e: Exception) { 81 | handleError(e) 82 | } 83 | 84 | suspend fun deleteTask(id: String): Either = try { 85 | client.delete("$BASE_ENDPOINT/todos/$id") 86 | 87 | Either.Right(true) 88 | } catch (e: Exception) { 89 | handleError(e) 90 | } 91 | 92 | private fun handleError(exception: Exception): Either = 93 | if (exception is BadResponseStatusException) { 94 | if (exception.statusCode.value == 404) { 95 | Either.Left(ItemNotFoundError) 96 | } else { 97 | Either.Left(UnknownError(exception.statusCode.value)) 98 | } 99 | } else { 100 | Either.Left(NetworkError) 101 | } 102 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/xurxodev/integrationtesting/error/ApiError.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.error 2 | 3 | sealed class ApiError 4 | data class UnknownError(val code: Int) : ApiError() 5 | object NetworkError : ApiError() 6 | object ItemNotFoundError : ApiError() -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/xurxodev/integrationtesting/model/Task.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Task( 7 | val id: Int, 8 | val userId: Int, 9 | val title: String, 10 | val completed: Boolean 11 | ) 12 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/TodoApiClientShould.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting 2 | 3 | import com.xurxodev.integrationtesting.common.api.TodoApiMockEngine 4 | import com.xurxodev.integrationtesting.common.coroutines.runTest 5 | import com.xurxodev.integrationtesting.common.responses.addTaskRequest 6 | import com.xurxodev.integrationtesting.common.responses.addTaskResponse 7 | import com.xurxodev.integrationtesting.common.responses.getTaskByIdResponse 8 | import com.xurxodev.integrationtesting.common.responses.getTasksResponse 9 | import com.xurxodev.integrationtesting.common.responses.updateTaskRequest 10 | import com.xurxodev.integrationtesting.common.responses.updateTaskResponse 11 | import com.xurxodev.integrationtesting.error.UnknownError 12 | import com.xurxodev.integrationtesting.error.ItemNotFoundError 13 | import com.xurxodev.integrationtesting.model.Task 14 | import todoapiclient.fold 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | import kotlin.test.assertFalse 18 | import kotlin.test.assertTrue 19 | import kotlin.test.fail 20 | 21 | class TodoApiClientShould { 22 | companion object { 23 | private const val ANY_TASK_ID = "1" 24 | 25 | private const val ALL_TASK_SEGMENT = "/todos" 26 | private const val TASK_SEGMENT = "/todos/$ANY_TASK_ID" 27 | 28 | private val ANY_TASK = Task(1, 1, "delectus aut autem", false) 29 | } 30 | 31 | private val todoApiMockEngine = TodoApiMockEngine() 32 | 33 | @Test 34 | fun `send accept header`() = runTest { 35 | val apiClient = givenAMockTodoApiClient(ALL_TASK_SEGMENT, getTasksResponse()) 36 | 37 | apiClient.getAllTasks() 38 | 39 | todoApiMockEngine.verifyRequestContainsHeader("Accept", "application/json") 40 | } 41 | 42 | @Test 43 | fun `send request with get http verb getting all task`() = runTest { 44 | val apiClient = givenAMockTodoApiClient(ALL_TASK_SEGMENT, getTasksResponse()) 45 | 46 | apiClient.getAllTasks() 47 | 48 | todoApiMockEngine.verifyGetRequest() 49 | } 50 | 51 | @Test 52 | fun `return tasks and parses it properly`() = runTest { 53 | val apiClient = givenAMockTodoApiClient(ALL_TASK_SEGMENT, getTasksResponse()) 54 | 55 | val tasksResponse = apiClient.getAllTasks() 56 | 57 | tasksResponse.fold( 58 | { left -> fail("Should return right but was left: $left") }, 59 | { right -> 60 | assertEquals(4, right.size.toLong()) 61 | assertTaskContainsExpectedValues(right[0]) 62 | }) 63 | } 64 | 65 | @Test 66 | fun `return http error 500 if server response internal server error getting all task`() = 67 | runTest { 68 | val apiClient = givenAMockTodoApiClient(ALL_TASK_SEGMENT, httpStatusCode = 500) 69 | 70 | val tasksResponse = apiClient.getAllTasks() 71 | 72 | tasksResponse.fold( 73 | { left -> assertEquals(UnknownError(500), left) }, 74 | { right -> fail("Should return left but was right: $right") }) 75 | } 76 | 77 | @Test 78 | fun `send request with get http verb getting getting by id`() = runTest { 79 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, getTaskByIdResponse()) 80 | 81 | apiClient.getTasksById(ANY_TASK_ID) 82 | 83 | todoApiMockEngine.verifyGetRequest() 84 | } 85 | 86 | @Test 87 | fun `return task and parses it properly getting by id`() = runTest { 88 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, getTaskByIdResponse()) 89 | 90 | val taskResponse = apiClient.getTasksById(ANY_TASK_ID) 91 | 92 | taskResponse.fold( 93 | { left -> fail("Should return right but was left: $left") }, 94 | { right -> 95 | assertTaskContainsExpectedValues(right) 96 | }) 97 | } 98 | 99 | @Test 100 | fun `return item not found error if there is no task with the passed id`() = runTest { 101 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 404) 102 | 103 | val taskResponse = apiClient.getTasksById(ANY_TASK_ID) 104 | 105 | taskResponse.fold( 106 | { left -> assertEquals(ItemNotFoundError, left) }, 107 | { right -> fail("Should return left but was right: $right") }) 108 | } 109 | 110 | @Test 111 | fun `return http error 500 if server response internal server error getting task by id`() = 112 | runTest { 113 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 500) 114 | 115 | val taskResponse = apiClient.getTasksById(ANY_TASK_ID) 116 | 117 | taskResponse.fold( 118 | { left -> assertEquals(UnknownError(500), left) }, 119 | { right -> fail("Should return left but was right: $right") }) 120 | } 121 | 122 | @Test 123 | fun `send request with post http verb adding a new task`() = runTest { 124 | val apiClient = 125 | givenAMockTodoApiClient(ALL_TASK_SEGMENT, addTaskResponse(), httpStatusCode = 201) 126 | 127 | apiClient.addTask(ANY_TASK) 128 | 129 | todoApiMockEngine.verifyPostRequest() 130 | } 131 | 132 | @Test 133 | fun `send the correct body adding a new task`() = runTest { 134 | val apiClient = 135 | givenAMockTodoApiClient(ALL_TASK_SEGMENT, addTaskResponse(), httpStatusCode = 201) 136 | 137 | apiClient.addTask(ANY_TASK) 138 | 139 | todoApiMockEngine.verifyRequestBody(addTaskRequest()) 140 | } 141 | 142 | @Test 143 | fun `return task and parses it properly adding a new task`() = runTest { 144 | val apiClient = givenAMockTodoApiClient(ALL_TASK_SEGMENT, addTaskResponse()) 145 | 146 | val taskResponse = apiClient.addTask(ANY_TASK) 147 | 148 | taskResponse.fold( 149 | { left -> fail("Should return right but was left: $left") }, 150 | { right -> 151 | assertTaskContainsExpectedValues(right) 152 | }) 153 | } 154 | 155 | @Test 156 | fun `return http error 500 if server response internal server error adding a new task`() = 157 | runTest { 158 | val apiClient = givenAMockTodoApiClient(ALL_TASK_SEGMENT, httpStatusCode = 500) 159 | 160 | val taskResponse = apiClient.addTask(ANY_TASK) 161 | 162 | taskResponse.fold( 163 | { left -> assertEquals(UnknownError(500), left) }, 164 | { right -> fail("Should return left but was right: $right") }) 165 | } 166 | 167 | @Test 168 | fun `send request with put http verb updating a task`() = runTest { 169 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, updateTaskResponse()) 170 | 171 | apiClient.updateTask(ANY_TASK) 172 | 173 | todoApiMockEngine.verifyPutRequest() 174 | } 175 | 176 | @Test 177 | fun `send the correct body updating a new task`() = runTest { 178 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, updateTaskResponse()) 179 | 180 | apiClient.updateTask(ANY_TASK) 181 | 182 | todoApiMockEngine.verifyRequestBody(updateTaskRequest()) 183 | } 184 | 185 | @Test 186 | fun `return task and parses it properly updating a new task`() = runTest { 187 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, addTaskResponse()) 188 | 189 | val taskResponse = apiClient.updateTask(ANY_TASK) 190 | 191 | taskResponse.fold( 192 | { left -> fail("Should return right but was left: $left") }, 193 | { right -> 194 | assertTaskContainsExpectedValues(right) 195 | }) 196 | } 197 | 198 | @Test 199 | fun `return item not found error if there is no task updating it`() = runTest { 200 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 404) 201 | 202 | val taskResponse = apiClient.updateTask(ANY_TASK) 203 | 204 | taskResponse.fold( 205 | { left -> assertEquals(ItemNotFoundError, left) }, 206 | { right -> fail("Should return left but was right: $right") }) 207 | } 208 | 209 | @Test 210 | fun `return http error 500 if server response internal server error updating a task`() = 211 | runTest { 212 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 500) 213 | 214 | val taskResponse = apiClient.updateTask(ANY_TASK) 215 | 216 | taskResponse.fold( 217 | { left -> assertEquals(UnknownError(500), left) }, 218 | { right -> fail("Should return left but was right: $right") }) 219 | } 220 | 221 | @Test 222 | fun `send request with delete http verb deleting a task`() = runTest { 223 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 200) 224 | 225 | apiClient.deleteTask(ANY_TASK_ID) 226 | 227 | todoApiMockEngine.verifyDeleteRequest() 228 | } 229 | 230 | @Test 231 | fun `return item not found error if there is no task deleting it`() = runTest { 232 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 404) 233 | 234 | val taskResponse = apiClient.deleteTask(ANY_TASK_ID) 235 | 236 | taskResponse.fold( 237 | { left -> assertEquals(ItemNotFoundError, left) }, 238 | { right -> fail("Should return left but was right: $right") }) 239 | } 240 | 241 | @Test 242 | fun `return http error 500 if server response internal server error deleting a task`() = 243 | runTest { 244 | val apiClient = givenAMockTodoApiClient(TASK_SEGMENT, httpStatusCode = 500) 245 | 246 | val taskResponse = apiClient.deleteTask(ANY_TASK_ID) 247 | 248 | taskResponse.fold( 249 | { left -> assertEquals(UnknownError(500), left) }, 250 | { right -> fail("Should return left but was right: $right") }) 251 | } 252 | 253 | private fun assertTaskContainsExpectedValues(task: Task?) { 254 | assertTrue(task != null) 255 | assertEquals(task.id, 1) 256 | assertEquals(task.userId, 1) 257 | assertEquals(task.title, "delectus aut autem") 258 | assertFalse(task.completed) 259 | } 260 | 261 | private fun givenAMockTodoApiClient( 262 | endpointSegment: String, 263 | responseBody: String = "", 264 | httpStatusCode: Int = 200 265 | ): TodoApiClient { 266 | todoApiMockEngine.enqueueMockResponse(endpointSegment, responseBody, httpStatusCode) 267 | 268 | return TodoApiClient(todoApiMockEngine.get()) 269 | } 270 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/api/MockResponse.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.api 2 | 3 | data class MockResponse( 4 | val endpointSegment: String, 5 | val responseBody: String, 6 | val httpStatusCode: Int = 200 7 | ) -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/api/TodoApiMockEngine.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.api 2 | 3 | import io.ktor.client.engine.mock.MockEngine 4 | import io.ktor.client.engine.mock.MockHttpRequest 5 | import io.ktor.client.engine.mock.MockHttpResponse 6 | import io.ktor.content.TextContent 7 | import io.ktor.http.ContentType 8 | import io.ktor.http.HttpHeaders 9 | import io.ktor.http.HttpMethod 10 | import io.ktor.http.HttpStatusCode 11 | import io.ktor.http.fullPath 12 | import io.ktor.http.headersOf 13 | import kotlinx.coroutines.io.ByteReadChannel 14 | import kotlinx.io.charsets.Charsets 15 | import kotlinx.io.core.toByteArray 16 | import kotlin.test.assertEquals 17 | 18 | class TodoApiMockEngine { 19 | private lateinit var mockResponse: MockResponse 20 | private var lastRequest: MockHttpRequest? = null 21 | 22 | fun enqueueMockResponse( 23 | endpointSegment: String, 24 | responseBody: String, 25 | httpStatusCode: Int = 200 26 | ) { 27 | mockResponse = MockResponse(endpointSegment, responseBody, httpStatusCode) 28 | } 29 | 30 | fun get() = MockEngine { 31 | lastRequest = this 32 | 33 | when (url.encodedPath) { 34 | "${mockResponse.endpointSegment}" -> { 35 | MockHttpResponse( 36 | call, 37 | HttpStatusCode.fromValue(mockResponse.httpStatusCode), 38 | ByteReadChannel(mockResponse.responseBody.toByteArray(Charsets.UTF_8)), 39 | headersOf(HttpHeaders.ContentType to listOf(ContentType.Application.Json.toString())) 40 | ) 41 | } 42 | else -> { 43 | error("Unhandled ${url.fullPath}") 44 | } 45 | } 46 | } 47 | 48 | fun verifyRequestContainsHeader(key: String, expectedValue: String) { 49 | val value = lastRequest!!.headers[key] 50 | assertEquals(expectedValue, value) 51 | } 52 | 53 | fun verifyRequestBody(addTaskRequest: String) { 54 | val body = (lastRequest!!.content as TextContent).text 55 | 56 | assertEquals(addTaskRequest, body) 57 | } 58 | 59 | fun verifyGetRequest() { 60 | assertEquals(HttpMethod.Get.value, lastRequest!!.method.value) 61 | } 62 | 63 | fun verifyPostRequest() { 64 | assertEquals(HttpMethod.Post.value, lastRequest!!.method.value) 65 | } 66 | 67 | fun verifyPutRequest() { 68 | assertEquals(HttpMethod.Put.value, lastRequest!!.method.value) 69 | } 70 | 71 | fun verifyDeleteRequest() { 72 | assertEquals(HttpMethod.Delete.value, lastRequest!!.method.value) 73 | } 74 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/coroutines/runTest.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.coroutines 2 | 3 | internal expect fun runTest(block: suspend () -> T): T -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/responses/addTaskRequest.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.responses 2 | 3 | fun addTaskRequest() = "{\"id\":1,\"userId\":1,\"title\":\"delectus aut autem\",\"completed\":false}" -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/responses/addTaskResponse.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.responses 2 | 3 | fun addTaskResponse() = 4 | "{\n" + 5 | " \"userId\": 1,\n" + 6 | " \"id\": 1,\n" + 7 | " \"title\": \"delectus aut autem\",\n" + 8 | " \"completed\": false\n" + 9 | "}" -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/responses/getTaskByIdResponse.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.responses 2 | 3 | fun getTaskByIdResponse() = 4 | "{\n" + 5 | " \"userId\": 1,\n" + 6 | " \"id\": 1,\n" + 7 | " \"title\": \"delectus aut autem\",\n" + 8 | " \"completed\": false\n" + 9 | "}" -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/responses/getTasksResponse.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.responses 2 | 3 | fun getTasksResponse() = 4 | "[{\n" + 5 | " \"userId\": 1,\n" + 6 | " \"id\": 1,\n" + 7 | " \"title\": \"delectus aut autem\",\n" + 8 | " \"completed\": false\n" + 9 | "}," + 10 | " {\n" + 11 | " \"userId\": 1,\n" + 12 | " \"id\": 2,\n" + 13 | " \"title\": \"quis ut nam facilis et officia qui\",\n" + 14 | " \"completed\": false\n" + 15 | "}, " + 16 | "{\n" + 17 | " \"userId\": 2,\n" + 18 | " \"id\": 3,\n" + 19 | " \"title\": \"fugiat veniam minus\",\n" + 20 | " \"completed\": false\n" + 21 | "}," + 22 | "{\n" + 23 | " \"userId\": 2,\n" + 24 | " \"id\": 4,\n" + 25 | " \"title\": \"et porro tempora\",\n" + 26 | " \"completed\": true\n" + 27 | "}]" -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/responses/updateTaskRequest.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.responses 2 | 3 | fun updateTaskRequest() = "{\"id\":1,\"userId\":1,\"title\":\"delectus aut autem\",\"completed\":false}" -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/xurxodev/integrationtesting/common/responses/updateTaskResponse.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.responses 2 | 3 | fun updateTaskResponse() = 4 | "{\n" + 5 | " \"userId\": 1,\n" + 6 | " \"id\": 1,\n" + 7 | " \"title\": \"delectus aut autem\",\n" + 8 | " \"completed\": false\n" + 9 | "}" -------------------------------------------------------------------------------- /src/commonTest/resources/getTasksResponse.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "userId": 1, 3 | "id": 1, 4 | "title": "delectus aut autem", 5 | "completed": false 6 | }, { 7 | "userId": 1, 8 | "id": 2, 9 | "title": "quis ut nam facilis et officia qui", 10 | "completed": false 11 | }, { 12 | "userId": 1, 13 | "id": 3, 14 | "title": "fugiat veniam minus", 15 | "completed": false 16 | }, { 17 | "userId": 1, 18 | "id": 4, 19 | "title": "et porro tempora", 20 | "completed": true 21 | }, { 22 | "userId": 1, 23 | "id": 5, 24 | "title": "laboriosam mollitia et enim quasi adipisci quia provident illum", 25 | "completed": false 26 | }, { 27 | "userId": 1, 28 | "id": 6, 29 | "title": "qui ullam ratione quibusdam voluptatem quia omnis", 30 | "completed": false 31 | }, { 32 | "userId": 1, 33 | "id": 7, 34 | "title": "illo expedita consequatur quia in", 35 | "completed": false 36 | }, { 37 | "userId": 1, 38 | "id": 8, 39 | "title": "quo adipisci enim quam ut ab", 40 | "completed": true 41 | }, { 42 | "userId": 1, 43 | "id": 9, 44 | "title": "molestiae perspiciatis ipsa", 45 | "completed": false 46 | }, { 47 | "userId": 1, 48 | "id": 10, 49 | "title": "illo est ratione doloremque quia maiores aut", 50 | "completed": true 51 | }, { 52 | "userId": 1, 53 | "id": 11, 54 | "title": "vero rerum temporibus dolor", 55 | "completed": true 56 | }, { 57 | "userId": 1, 58 | "id": 12, 59 | "title": "ipsa repellendus fugit nisi", 60 | "completed": true 61 | }, { 62 | "userId": 1, 63 | "id": 13, 64 | "title": "et doloremque nulla", 65 | "completed": false 66 | }, { 67 | "userId": 1, 68 | "id": 14, 69 | "title": "repellendus sunt dolores architecto voluptatum", 70 | "completed": true 71 | }, { 72 | "userId": 1, 73 | "id": 15, 74 | "title": "ab voluptatum amet voluptas", 75 | "completed": true 76 | }, { 77 | "userId": 1, 78 | "id": 16, 79 | "title": "accusamus eos facilis sint et aut voluptatem", 80 | "completed": true 81 | }, { 82 | "userId": 1, 83 | "id": 17, 84 | "title": "quo laboriosam deleniti aut qui", 85 | "completed": true 86 | }, { 87 | "userId": 1, 88 | "id": 18, 89 | "title": "dolorum est consequatur ea mollitia in culpa", 90 | "completed": false 91 | }, { 92 | "userId": 1, 93 | "id": 19, 94 | "title": "molestiae ipsa aut voluptatibus pariatur dolor nihil", 95 | "completed": true 96 | }, { 97 | "userId": 1, 98 | "id": 20, 99 | "title": "ullam nobis libero sapiente ad optio sint", 100 | "completed": true 101 | }, { 102 | "userId": 2, 103 | "id": 21, 104 | "title": "suscipit repellat esse quibusdam voluptatem incidunt", 105 | "completed": false 106 | }, { 107 | "userId": 2, 108 | "id": 22, 109 | "title": "distinctio vitae autem nihil ut molestias quo", 110 | "completed": true 111 | }, { 112 | "userId": 2, 113 | "id": 23, 114 | "title": "et itaque necessitatibus maxime molestiae qui quas velit", 115 | "completed": false 116 | }, { 117 | "userId": 2, 118 | "id": 24, 119 | "title": "adipisci non ad dicta qui amet quaerat doloribus ea", 120 | "completed": false 121 | }, { 122 | "userId": 2, 123 | "id": 25, 124 | "title": "voluptas quo tenetur perspiciatis explicabo natus", 125 | "completed": true 126 | }, { 127 | "userId": 2, 128 | "id": 26, 129 | "title": "aliquam aut quasi", 130 | "completed": true 131 | }, { 132 | "userId": 2, 133 | "id": 27, 134 | "title": "veritatis pariatur delectus", 135 | "completed": true 136 | }, { 137 | "userId": 2, 138 | "id": 28, 139 | "title": "nesciunt totam sit blanditiis sit", 140 | "completed": false 141 | }, { 142 | "userId": 2, 143 | "id": 29, 144 | "title": "laborum aut in quam", 145 | "completed": false 146 | }, { 147 | "userId": 2, 148 | "id": 30, 149 | "title": "nemo perspiciatis repellat ut dolor libero commodi blanditiis omnis", 150 | "completed": true 151 | }, { 152 | "userId": 2, 153 | "id": 31, 154 | "title": "repudiandae totam in est sint facere fuga", 155 | "completed": false 156 | }, { 157 | "userId": 2, 158 | "id": 32, 159 | "title": "earum doloribus ea doloremque quis", 160 | "completed": false 161 | }, { 162 | "userId": 2, 163 | "id": 33, 164 | "title": "sint sit aut vero", 165 | "completed": false 166 | }, { 167 | "userId": 2, 168 | "id": 34, 169 | "title": "porro aut necessitatibus eaque distinctio", 170 | "completed": false 171 | }, { 172 | "userId": 2, 173 | "id": 35, 174 | "title": "repellendus veritatis molestias dicta incidunt", 175 | "completed": true 176 | }, { 177 | "userId": 2, 178 | "id": 36, 179 | "title": "excepturi deleniti adipisci voluptatem et neque optio illum ad", 180 | "completed": true 181 | }, { 182 | "userId": 2, 183 | "id": 37, 184 | "title": "sunt cum tempora", 185 | "completed": false 186 | }, { 187 | "userId": 2, 188 | "id": 38, 189 | "title": "totam quia non", 190 | "completed": false 191 | }, { 192 | "userId": 2, 193 | "id": 39, 194 | "title": "doloremque quibusdam asperiores libero corrupti illum qui omnis", 195 | "completed": false 196 | }, { 197 | "userId": 2, 198 | "id": 40, 199 | "title": "totam atque quo nesciunt", 200 | "completed": true 201 | }, { 202 | "userId": 3, 203 | "id": 41, 204 | "title": "aliquid amet impedit consequatur aspernatur placeat eaque fugiat suscipit", 205 | "completed": false 206 | }, { 207 | "userId": 3, 208 | "id": 42, 209 | "title": "rerum perferendis error quia ut eveniet", 210 | "completed": false 211 | }, { 212 | "userId": 3, 213 | "id": 43, 214 | "title": "tempore ut sint quis recusandae", 215 | "completed": true 216 | }, { 217 | "userId": 3, 218 | "id": 44, 219 | "title": "cum debitis quis accusamus doloremque ipsa natus sapiente omnis", 220 | "completed": true 221 | }, { 222 | "userId": 3, 223 | "id": 45, 224 | "title": "velit soluta adipisci molestias reiciendis harum", 225 | "completed": false 226 | }, { 227 | "userId": 3, 228 | "id": 46, 229 | "title": "vel voluptatem repellat nihil placeat corporis", 230 | "completed": false 231 | }, { 232 | "userId": 3, 233 | "id": 47, 234 | "title": "nam qui rerum fugiat accusamus", 235 | "completed": false 236 | }, { 237 | "userId": 3, 238 | "id": 48, 239 | "title": "sit reprehenderit omnis quia", 240 | "completed": false 241 | }, { 242 | "userId": 3, 243 | "id": 49, 244 | "title": "ut necessitatibus aut maiores debitis officia blanditiis velit et", 245 | "completed": false 246 | }, { 247 | "userId": 3, 248 | "id": 50, 249 | "title": "cupiditate necessitatibus ullam aut quis dolor voluptate", 250 | "completed": true 251 | }, { 252 | "userId": 3, 253 | "id": 51, 254 | "title": "distinctio exercitationem ab doloribus", 255 | "completed": false 256 | }, { 257 | "userId": 3, 258 | "id": 52, 259 | "title": "nesciunt dolorum quis recusandae ad pariatur ratione", 260 | "completed": false 261 | }, { 262 | "userId": 3, 263 | "id": 53, 264 | "title": "qui labore est occaecati recusandae aliquid quam", 265 | "completed": false 266 | }, { 267 | "userId": 3, 268 | "id": 54, 269 | "title": "quis et est ut voluptate quam dolor", 270 | "completed": true 271 | }, { 272 | "userId": 3, 273 | "id": 55, 274 | "title": "voluptatum omnis minima qui occaecati provident nulla voluptatem ratione", 275 | "completed": true 276 | }, { 277 | "userId": 3, 278 | "id": 56, 279 | "title": "deleniti ea temporibus enim", 280 | "completed": true 281 | }, { 282 | "userId": 3, 283 | "id": 57, 284 | "title": "pariatur et magnam ea doloribus similique voluptatem rerum quia", 285 | "completed": false 286 | }, { 287 | "userId": 3, 288 | "id": 58, 289 | "title": "est dicta totam qui explicabo doloribus qui dignissimos", 290 | "completed": false 291 | }, { 292 | "userId": 3, 293 | "id": 59, 294 | "title": "perspiciatis velit id laborum placeat iusto et aliquam odio", 295 | "completed": false 296 | }, { 297 | "userId": 3, 298 | "id": 60, 299 | "title": "et sequi qui architecto ut adipisci", 300 | "completed": true 301 | }, { 302 | "userId": 4, 303 | "id": 61, 304 | "title": "odit optio omnis qui sunt", 305 | "completed": true 306 | }, { 307 | "userId": 4, 308 | "id": 62, 309 | "title": "et placeat et tempore aspernatur sint numquam", 310 | "completed": false 311 | }, { 312 | "userId": 4, 313 | "id": 63, 314 | "title": "doloremque aut dolores quidem fuga qui nulla", 315 | "completed": true 316 | }, { 317 | "userId": 4, 318 | "id": 64, 319 | "title": "voluptas consequatur qui ut quia magnam nemo esse", 320 | "completed": false 321 | }, { 322 | "userId": 4, 323 | "id": 65, 324 | "title": "fugiat pariatur ratione ut asperiores necessitatibus magni", 325 | "completed": false 326 | }, { 327 | "userId": 4, 328 | "id": 66, 329 | "title": "rerum eum molestias autem voluptatum sit optio", 330 | "completed": false 331 | }, { 332 | "userId": 4, 333 | "id": 67, 334 | "title": "quia voluptatibus voluptatem quos similique maiores repellat", 335 | "completed": false 336 | }, { 337 | "userId": 4, 338 | "id": 68, 339 | "title": "aut id perspiciatis voluptatem iusto", 340 | "completed": false 341 | }, { 342 | "userId": 4, 343 | "id": 69, 344 | "title": "doloribus sint dolorum ab adipisci itaque dignissimos aliquam suscipit", 345 | "completed": false 346 | }, { 347 | "userId": 4, 348 | "id": 70, 349 | "title": "ut sequi accusantium et mollitia delectus sunt", 350 | "completed": false 351 | }, { 352 | "userId": 4, 353 | "id": 71, 354 | "title": "aut velit saepe ullam", 355 | "completed": false 356 | }, { 357 | "userId": 4, 358 | "id": 72, 359 | "title": "praesentium facilis facere quis harum voluptatibus voluptatem eum", 360 | "completed": false 361 | }, { 362 | "userId": 4, 363 | "id": 73, 364 | "title": "sint amet quia totam corporis qui exercitationem commodi", 365 | "completed": true 366 | }, { 367 | "userId": 4, 368 | "id": 74, 369 | "title": "expedita tempore nobis eveniet laborum maiores", 370 | "completed": false 371 | }, { 372 | "userId": 4, 373 | "id": 75, 374 | "title": "occaecati adipisci est possimus totam", 375 | "completed": false 376 | }, { 377 | "userId": 4, 378 | "id": 76, 379 | "title": "sequi dolorem sed", 380 | "completed": true 381 | }, { 382 | "userId": 4, 383 | "id": 77, 384 | "title": "maiores aut nesciunt delectus exercitationem vel assumenda eligendi at", 385 | "completed": false 386 | }, { 387 | "userId": 4, 388 | "id": 78, 389 | "title": "reiciendis est magnam amet nemo iste recusandae impedit quaerat", 390 | "completed": false 391 | }, { 392 | "userId": 4, 393 | "id": 79, 394 | "title": "eum ipsa maxime ut", 395 | "completed": true 396 | }, { 397 | "userId": 4, 398 | "id": 80, 399 | "title": "tempore molestias dolores rerum sequi voluptates ipsum consequatur", 400 | "completed": true 401 | }, { 402 | "userId": 5, 403 | "id": 81, 404 | "title": "suscipit qui totam", 405 | "completed": true 406 | }, { 407 | "userId": 5, 408 | "id": 82, 409 | "title": "voluptates eum voluptas et dicta", 410 | "completed": false 411 | }, { 412 | "userId": 5, 413 | "id": 83, 414 | "title": "quidem at rerum quis ex aut sit quam", 415 | "completed": true 416 | }, { 417 | "userId": 5, 418 | "id": 84, 419 | "title": "sunt veritatis ut voluptate", 420 | "completed": false 421 | }, { 422 | "userId": 5, 423 | "id": 85, 424 | "title": "et quia ad iste a", 425 | "completed": true 426 | }, { 427 | "userId": 5, 428 | "id": 86, 429 | "title": "incidunt ut saepe autem", 430 | "completed": true 431 | }, { 432 | "userId": 5, 433 | "id": 87, 434 | "title": "laudantium quae eligendi consequatur quia et vero autem", 435 | "completed": true 436 | }, { 437 | "userId": 5, 438 | "id": 88, 439 | "title": "vitae aut excepturi laboriosam sint aliquam et et accusantium", 440 | "completed": false 441 | }, { 442 | "userId": 5, 443 | "id": 89, 444 | "title": "sequi ut omnis et", 445 | "completed": true 446 | }, { 447 | "userId": 5, 448 | "id": 90, 449 | "title": "molestiae nisi accusantium tenetur dolorem et", 450 | "completed": true 451 | }, { 452 | "userId": 5, 453 | "id": 91, 454 | "title": "nulla quis consequatur saepe qui id expedita", 455 | "completed": true 456 | }, { 457 | "userId": 5, 458 | "id": 92, 459 | "title": "in omnis laboriosam", 460 | "completed": true 461 | }, { 462 | "userId": 5, 463 | "id": 93, 464 | "title": "odio iure consequatur molestiae quibusdam necessitatibus quia sint", 465 | "completed": true 466 | }, { 467 | "userId": 5, 468 | "id": 94, 469 | "title": "facilis modi saepe mollitia", 470 | "completed": false 471 | }, { 472 | "userId": 5, 473 | "id": 95, 474 | "title": "vel nihil et molestiae iusto assumenda nemo quo ut", 475 | "completed": true 476 | }, { 477 | "userId": 5, 478 | "id": 96, 479 | "title": "nobis suscipit ducimus enim asperiores voluptas", 480 | "completed": false 481 | }, { 482 | "userId": 5, 483 | "id": 97, 484 | "title": "dolorum laboriosam eos qui iure aliquam", 485 | "completed": false 486 | }, { 487 | "userId": 5, 488 | "id": 98, 489 | "title": "debitis accusantium ut quo facilis nihil quis sapiente necessitatibus", 490 | "completed": true 491 | }, { 492 | "userId": 5, 493 | "id": 99, 494 | "title": "neque voluptates ratione", 495 | "completed": false 496 | }, { 497 | "userId": 5, 498 | "id": 100, 499 | "title": "excepturi a et neque qui expedita vel voluptate", 500 | "completed": false 501 | }, { 502 | "userId": 6, 503 | "id": 101, 504 | "title": "explicabo enim cumque porro aperiam occaecati minima", 505 | "completed": false 506 | }, { 507 | "userId": 6, 508 | "id": 102, 509 | "title": "sed ab consequatur", 510 | "completed": false 511 | }, { 512 | "userId": 6, 513 | "id": 103, 514 | "title": "non sunt delectus illo nulla tenetur enim omnis", 515 | "completed": false 516 | }, { 517 | "userId": 6, 518 | "id": 104, 519 | "title": "excepturi non laudantium quo", 520 | "completed": false 521 | }, { 522 | "userId": 6, 523 | "id": 105, 524 | "title": "totam quia dolorem et illum repellat voluptas optio", 525 | "completed": true 526 | }, { 527 | "userId": 6, 528 | "id": 106, 529 | "title": "ad illo quis voluptatem temporibus", 530 | "completed": true 531 | }, { 532 | "userId": 6, 533 | "id": 107, 534 | "title": "praesentium facilis omnis laudantium fugit ad iusto nihil nesciunt", 535 | "completed": false 536 | }, { 537 | "userId": 6, 538 | "id": 108, 539 | "title": "a eos eaque nihil et exercitationem incidunt delectus", 540 | "completed": true 541 | }, { 542 | "userId": 6, 543 | "id": 109, 544 | "title": "autem temporibus harum quisquam in culpa", 545 | "completed": true 546 | }, { 547 | "userId": 6, 548 | "id": 110, 549 | "title": "aut aut ea corporis", 550 | "completed": true 551 | }, { 552 | "userId": 6, 553 | "id": 111, 554 | "title": "magni accusantium labore et id quis provident", 555 | "completed": false 556 | }, { 557 | "userId": 6, 558 | "id": 112, 559 | "title": "consectetur impedit quisquam qui deserunt non rerum consequuntur eius", 560 | "completed": false 561 | }, { 562 | "userId": 6, 563 | "id": 113, 564 | "title": "quia atque aliquam sunt impedit voluptatum rerum assumenda nisi", 565 | "completed": false 566 | }, { 567 | "userId": 6, 568 | "id": 114, 569 | "title": "cupiditate quos possimus corporis quisquam exercitationem beatae", 570 | "completed": false 571 | }, { 572 | "userId": 6, 573 | "id": 115, 574 | "title": "sed et ea eum", 575 | "completed": false 576 | }, { 577 | "userId": 6, 578 | "id": 116, 579 | "title": "ipsa dolores vel facilis ut", 580 | "completed": true 581 | }, { 582 | "userId": 6, 583 | "id": 117, 584 | "title": "sequi quae est et qui qui eveniet asperiores", 585 | "completed": false 586 | }, { 587 | "userId": 6, 588 | "id": 118, 589 | "title": "quia modi consequatur vero fugiat", 590 | "completed": false 591 | }, { 592 | "userId": 6, 593 | "id": 119, 594 | "title": "corporis ducimus ea perspiciatis iste", 595 | "completed": false 596 | }, { 597 | "userId": 6, 598 | "id": 120, 599 | "title": "dolorem laboriosam vel voluptas et aliquam quasi", 600 | "completed": false 601 | }, { 602 | "userId": 7, 603 | "id": 121, 604 | "title": "inventore aut nihil minima laudantium hic qui omnis", 605 | "completed": true 606 | }, { 607 | "userId": 7, 608 | "id": 122, 609 | "title": "provident aut nobis culpa", 610 | "completed": true 611 | }, { 612 | "userId": 7, 613 | "id": 123, 614 | "title": "esse et quis iste est earum aut impedit", 615 | "completed": false 616 | }, { 617 | "userId": 7, 618 | "id": 124, 619 | "title": "qui consectetur id", 620 | "completed": false 621 | }, { 622 | "userId": 7, 623 | "id": 125, 624 | "title": "aut quasi autem iste tempore illum possimus", 625 | "completed": false 626 | }, { 627 | "userId": 7, 628 | "id": 126, 629 | "title": "ut asperiores perspiciatis veniam ipsum rerum saepe", 630 | "completed": true 631 | }, { 632 | "userId": 7, 633 | "id": 127, 634 | "title": "voluptatem libero consectetur rerum ut", 635 | "completed": true 636 | }, { 637 | "userId": 7, 638 | "id": 128, 639 | "title": "eius omnis est qui voluptatem autem", 640 | "completed": false 641 | }, { 642 | "userId": 7, 643 | "id": 129, 644 | "title": "rerum culpa quis harum", 645 | "completed": false 646 | }, { 647 | "userId": 7, 648 | "id": 130, 649 | "title": "nulla aliquid eveniet harum laborum libero alias ut unde", 650 | "completed": true 651 | }, { 652 | "userId": 7, 653 | "id": 131, 654 | "title": "qui ea incidunt quis", 655 | "completed": false 656 | }, { 657 | "userId": 7, 658 | "id": 132, 659 | "title": "qui molestiae voluptatibus velit iure harum quisquam", 660 | "completed": true 661 | }, { 662 | "userId": 7, 663 | "id": 133, 664 | "title": "et labore eos enim rerum consequatur sunt", 665 | "completed": true 666 | }, { 667 | "userId": 7, 668 | "id": 134, 669 | "title": "molestiae doloribus et laborum quod ea", 670 | "completed": false 671 | }, { 672 | "userId": 7, 673 | "id": 135, 674 | "title": "facere ipsa nam eum voluptates reiciendis vero qui", 675 | "completed": false 676 | }, { 677 | "userId": 7, 678 | "id": 136, 679 | "title": "asperiores illo tempora fuga sed ut quasi adipisci", 680 | "completed": false 681 | }, { 682 | "userId": 7, 683 | "id": 137, 684 | "title": "qui sit non", 685 | "completed": false 686 | }, { 687 | "userId": 7, 688 | "id": 138, 689 | "title": "placeat minima consequatur rem qui ut", 690 | "completed": true 691 | }, { 692 | "userId": 7, 693 | "id": 139, 694 | "title": "consequatur doloribus id possimus voluptas a voluptatem", 695 | "completed": false 696 | }, { 697 | "userId": 7, 698 | "id": 140, 699 | "title": "aut consectetur in blanditiis deserunt quia sed laboriosam", 700 | "completed": true 701 | }, { 702 | "userId": 8, 703 | "id": 141, 704 | "title": "explicabo consectetur debitis voluptates quas quae culpa rerum non", 705 | "completed": true 706 | }, { 707 | "userId": 8, 708 | "id": 142, 709 | "title": "maiores accusantium architecto necessitatibus reiciendis ea aut", 710 | "completed": true 711 | }, { 712 | "userId": 8, 713 | "id": 143, 714 | "title": "eum non recusandae cupiditate animi", 715 | "completed": false 716 | }, { 717 | "userId": 8, 718 | "id": 144, 719 | "title": "ut eum exercitationem sint", 720 | "completed": false 721 | }, { 722 | "userId": 8, 723 | "id": 145, 724 | "title": "beatae qui ullam incidunt voluptatem non nisi aliquam", 725 | "completed": false 726 | }, { 727 | "userId": 8, 728 | "id": 146, 729 | "title": "molestiae suscipit ratione nihil odio libero impedit vero totam", 730 | "completed": true 731 | }, { 732 | "userId": 8, 733 | "id": 147, 734 | "title": "eum itaque quod reprehenderit et facilis dolor autem ut", 735 | "completed": true 736 | }, { 737 | "userId": 8, 738 | "id": 148, 739 | "title": "esse quas et quo quasi exercitationem", 740 | "completed": false 741 | }, { 742 | "userId": 8, 743 | "id": 149, 744 | "title": "animi voluptas quod perferendis est", 745 | "completed": false 746 | }, { 747 | "userId": 8, 748 | "id": 150, 749 | "title": "eos amet tempore laudantium fugit a", 750 | "completed": false 751 | }, { 752 | "userId": 8, 753 | "id": 151, 754 | "title": "accusamus adipisci dicta qui quo ea explicabo sed vero", 755 | "completed": true 756 | }, { 757 | "userId": 8, 758 | "id": 152, 759 | "title": "odit eligendi recusandae doloremque cumque non", 760 | "completed": false 761 | }, { 762 | "userId": 8, 763 | "id": 153, 764 | "title": "ea aperiam consequatur qui repellat eos", 765 | "completed": false 766 | }, { 767 | "userId": 8, 768 | "id": 154, 769 | "title": "rerum non ex sapiente", 770 | "completed": true 771 | }, { 772 | "userId": 8, 773 | "id": 155, 774 | "title": "voluptatem nobis consequatur et assumenda magnam", 775 | "completed": true 776 | }, { 777 | "userId": 8, 778 | "id": 156, 779 | "title": "nam quia quia nulla repellat assumenda quibusdam sit nobis", 780 | "completed": true 781 | }, { 782 | "userId": 8, 783 | "id": 157, 784 | "title": "dolorem veniam quisquam deserunt repellendus", 785 | "completed": true 786 | }, { 787 | "userId": 8, 788 | "id": 158, 789 | "title": "debitis vitae delectus et harum accusamus aut deleniti a", 790 | "completed": true 791 | }, { 792 | "userId": 8, 793 | "id": 159, 794 | "title": "debitis adipisci quibusdam aliquam sed dolore ea praesentium nobis", 795 | "completed": true 796 | }, { 797 | "userId": 8, 798 | "id": 160, 799 | "title": "et praesentium aliquam est", 800 | "completed": false 801 | }, { 802 | "userId": 9, 803 | "id": 161, 804 | "title": "ex hic consequuntur earum omnis alias ut occaecati culpa", 805 | "completed": true 806 | }, { 807 | "userId": 9, 808 | "id": 162, 809 | "title": "omnis laboriosam molestias animi sunt dolore", 810 | "completed": true 811 | }, { 812 | "userId": 9, 813 | "id": 163, 814 | "title": "natus corrupti maxime laudantium et voluptatem laboriosam odit", 815 | "completed": false 816 | }, { 817 | "userId": 9, 818 | "id": 164, 819 | "title": "reprehenderit quos aut aut consequatur est sed", 820 | "completed": false 821 | }, { 822 | "userId": 9, 823 | "id": 165, 824 | "title": "fugiat perferendis sed aut quidem", 825 | "completed": false 826 | }, { 827 | "userId": 9, 828 | "id": 166, 829 | "title": "quos quo possimus suscipit minima ut", 830 | "completed": false 831 | }, { 832 | "userId": 9, 833 | "id": 167, 834 | "title": "et quis minus quo a asperiores molestiae", 835 | "completed": false 836 | }, { 837 | "userId": 9, 838 | "id": 168, 839 | "title": "recusandae quia qui sunt libero", 840 | "completed": false 841 | }, { 842 | "userId": 9, 843 | "id": 169, 844 | "title": "ea odio perferendis officiis", 845 | "completed": true 846 | }, { 847 | "userId": 9, 848 | "id": 170, 849 | "title": "quisquam aliquam quia doloribus aut", 850 | "completed": false 851 | }, { 852 | "userId": 9, 853 | "id": 171, 854 | "title": "fugiat aut voluptatibus corrupti deleniti velit iste odio", 855 | "completed": true 856 | }, { 857 | "userId": 9, 858 | "id": 172, 859 | "title": "et provident amet rerum consectetur et voluptatum", 860 | "completed": false 861 | }, { 862 | "userId": 9, 863 | "id": 173, 864 | "title": "harum ad aperiam quis", 865 | "completed": false 866 | }, { 867 | "userId": 9, 868 | "id": 174, 869 | "title": "similique aut quo", 870 | "completed": false 871 | }, { 872 | "userId": 9, 873 | "id": 175, 874 | "title": "laudantium eius officia perferendis provident perspiciatis asperiores", 875 | "completed": true 876 | }, { 877 | "userId": 9, 878 | "id": 176, 879 | "title": "magni soluta corrupti ut maiores rem quidem", 880 | "completed": false 881 | }, { 882 | "userId": 9, 883 | "id": 177, 884 | "title": "et placeat temporibus voluptas est tempora quos quibusdam", 885 | "completed": false 886 | }, { 887 | "userId": 9, 888 | "id": 178, 889 | "title": "nesciunt itaque commodi tempore", 890 | "completed": true 891 | }, { 892 | "userId": 9, 893 | "id": 179, 894 | "title": "omnis consequuntur cupiditate impedit itaque ipsam quo", 895 | "completed": true 896 | }, { 897 | "userId": 9, 898 | "id": 180, 899 | "title": "debitis nisi et dolorem repellat et", 900 | "completed": true 901 | }, { 902 | "userId": 10, 903 | "id": 181, 904 | "title": "ut cupiditate sequi aliquam fuga maiores", 905 | "completed": false 906 | }, { 907 | "userId": 10, 908 | "id": 182, 909 | "title": "inventore saepe cumque et aut illum enim", 910 | "completed": true 911 | }, { 912 | "userId": 10, 913 | "id": 183, 914 | "title": "omnis nulla eum aliquam distinctio", 915 | "completed": true 916 | }, { 917 | "userId": 10, 918 | "id": 184, 919 | "title": "molestias modi perferendis perspiciatis", 920 | "completed": false 921 | }, { 922 | "userId": 10, 923 | "id": 185, 924 | "title": "voluptates dignissimos sed doloribus animi quaerat aut", 925 | "completed": false 926 | }, { 927 | "userId": 10, 928 | "id": 186, 929 | "title": "explicabo odio est et", 930 | "completed": false 931 | }, { 932 | "userId": 10, 933 | "id": 187, 934 | "title": "consequuntur animi possimus", 935 | "completed": false 936 | }, { 937 | "userId": 10, 938 | "id": 188, 939 | "title": "vel non beatae est", 940 | "completed": true 941 | }, { 942 | "userId": 10, 943 | "id": 189, 944 | "title": "culpa eius et voluptatem et", 945 | "completed": true 946 | }, { 947 | "userId": 10, 948 | "id": 190, 949 | "title": "accusamus sint iusto et voluptatem exercitationem", 950 | "completed": true 951 | }, { 952 | "userId": 10, 953 | "id": 191, 954 | "title": "temporibus atque distinctio omnis eius impedit tempore molestias pariatur", 955 | "completed": true 956 | }, { 957 | "userId": 10, 958 | "id": 192, 959 | "title": "ut quas possimus exercitationem sint voluptates", 960 | "completed": false 961 | }, { 962 | "userId": 10, 963 | "id": 193, 964 | "title": "rerum debitis voluptatem qui eveniet tempora distinctio a", 965 | "completed": true 966 | }, { 967 | "userId": 10, 968 | "id": 194, 969 | "title": "sed ut vero sit molestiae", 970 | "completed": false 971 | }, { 972 | "userId": 10, 973 | "id": 195, 974 | "title": "rerum ex veniam mollitia voluptatibus pariatur", 975 | "completed": true 976 | }, { 977 | "userId": 10, 978 | "id": 196, 979 | "title": "consequuntur aut ut fugit similique", 980 | "completed": true 981 | }, { 982 | "userId": 10, 983 | "id": 197, 984 | "title": "dignissimos quo nobis earum saepe", 985 | "completed": true 986 | }, { 987 | "userId": 10, 988 | "id": 198, 989 | "title": "quis eius est sint explicabo", 990 | "completed": true 991 | }, { 992 | "userId": 10, 993 | "id": 199, 994 | "title": "numquam repellendus a magnam", 995 | "completed": true 996 | }, { 997 | "userId": 10, 998 | "id": 200, 999 | "title": "ipsam aperiam voluptates qui", 1000 | "completed": false 1001 | }] -------------------------------------------------------------------------------- /src/iosTest/kotlin/com/xurxodev/integrationtesting/common/coroutines/runTest.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.coroutines 2 | 3 | import kotlinx.coroutines.runBlocking 4 | 5 | internal actual fun runTest(block: suspend () -> T): T { 6 | return runBlocking { block() } 7 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/com/xurxodev/integrationtesting/common/coroutines/runTest.kt: -------------------------------------------------------------------------------- 1 | package com.xurxodev.integrationtesting.common.coroutines 2 | 3 | import kotlinx.coroutines.runBlocking 4 | 5 | internal actual fun runTest(block: suspend () -> T): T { 6 | return runBlocking { block() } 7 | } --------------------------------------------------------------------------------