{"id":16195950,"url":"https://github.com/pheymann/artie","last_synced_at":"2025-03-19T04:30:58.154Z","repository":{"id":57722066,"uuid":"102633753","full_name":"pheymann/artie","owner":"pheymann","description":"Scala test-framework for REST service refactorings","archived":false,"fork":false,"pushed_at":"2018-02-07T09:37:49.000Z","size":111,"stargazers_count":9,"open_issues_count":2,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-02-28T15:59:48.393Z","etag":null,"topics":["json","microservice","refactoring","response-comparison","rest","shapeless","test-automation","testing"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pheymann.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-06T16:44:40.000Z","updated_at":"2024-04-03T12:59:41.000Z","dependencies_parsed_at":"2022-08-29T23:00:15.110Z","dependency_job_id":null,"html_url":"https://github.com/pheymann/artie","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pheymann%2Fartie","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pheymann%2Fartie/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pheymann%2Fartie/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pheymann%2Fartie/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pheymann","download_url":"https://codeload.github.com/pheymann/artie/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243971156,"owners_count":20376784,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json","microservice","refactoring","response-comparison","rest","shapeless","test-automation","testing"],"created_at":"2024-10-10T08:46:08.863Z","updated_at":"2025-03-19T04:30:57.852Z","avatar_url":"https://github.com/pheymann.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://travis-ci.org/pheymann/artie.svg?branch=master)](https://travis-ci.org/pheymann/artie)\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.pheymann/artie_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.pheymann/artie_2.12)\n[![codecov.io](http://codecov.io/github/pheymann/artie/coverage.svg?branch=master)](http://codecov.io/github/pheymann/artie?branch=master)\n\n# artie {from rrt := rest-refactoring-test-framework}\nYou want to change a (legacy) REST service which has no tests and it is impossible to\nwrite some tests without rebuilding the whole thing? If so this tool may help you. It is\na small framework to generate REST request from different data sets, run them against\ntwo instances of your service (old and new) and compare the responses. It lets you write small\nand concise tests, the compiler will do the rest.\n\nThe only thing you have to do is:\n\n```Scala\nimport artie._\nimport artie.implicits._\n\n// write a refactoring spec\nobject MyServiceRefactoring extends RefactoringSpec(\"my-service\") {\n\n  // give some informations\n  val conf = Config(\"old-host\", 8080, \"new-host\", 8080)\n  val db   = mysql(\"db-host\", \"user\", \"pwd\")\n\n  // add some data\n  val providers = Providers ~\n    // random ages between 10 and 100\n    ('ages, provide[Int].random(10, 100) ~\n\n    // some user ids from a db\n    ('userIds, provide[Long].database.random(\"users_table\", \"user_id\", limit = 100, db)\n\n  // you have to provide `read` (see below)\n  check(\"get-user\", providers, conf, read[User]) { implicit r =\u003e p =\u003e\n    val userId = select('userIds, p).next\n    val ageO   = select('ages, p).nextOpt\n\n    // request builder\n    get(s\"/user/$userId\", Params \u003c\u0026\u003e (\"age\", ageO))\n  }\n}\n```\n\nAnd run it:\n\n```\n# if both instances behave the same\nsbt \"runMain MyServiceRefactoring\"\n\ntesting refactorings for my-service:\n  + check get-user\n    processed: 1 / 1\n\nSuccess: Total: 1; Succeeded: 1, Invalid: 0; Failed: 0\n\n# in presence of differences\nsbt \"it:runMain MyServiceRefactoring\"\n\ntesting refactorings for my-service:\n  + check get-user\n    processed: 1 / 1\n    \n    Get /user/0?age=20\n    {\n      city: \"Hamburg\" != \"New York\"\n    }\n\nFailed: Total: 1; Succeeded: 0, Invalid: 0; Failed: 1\n```\n\nHere `Invalid` indicates response pairs with the same error code (3.x.x, 4.x.x or 5.x.x). Invalide results\ndon't fail a test.\n\nFor some examples take a look [here](https://github.com/pheymann/artie/tree/master/examples/src/it/scala/examples).\n\n## Table of Contents\n - [Get This Framework](#get-this-framework)\n - [Dependencies](#dependencies)\n - [Documentation](#documentation)\n\n### Get This Framework\nYou can add it as dependency for Scala **2.11** and **2.12**:\n\n```Scala\n// take a look at the maven batch to find the latest version\nlibraryDependencies += \"com.github.pheymann\" %% \"artie\" % \u003cversion\u003e\n```\n\nor build it locally:\n\n```\ngit clone https://github.com/pheymann/artie.git\ncd artie\nsbt \"publishLocal\"\n```\n\n### Dependencies\nI tried to keep the dependencies to external libraries as small as possible. Currently this framework uses:\n  - [shapeless 2.3.2](https://github.com/milessabin/shapeless/)\n  - [scalaj-http 2.3.0](https://github.com/scalaj/scalaj-http/)\n\n## Documentation\nIn the following I'll describe the basic elements of a refactoring spec: test configuration (`TestConfig`), data providers (`Provider`) and multiple test cases (`check`), in more detail.\n\n### Table of Contents\n - [TestConfig](#testconfig)\n - [Providers](#providers)\n - [Read REST responses](#read-rest-responses)\n - [Data Selector](#data-selector)\n - [Request Builder](#request-builder)\n - [Test Suite](#test-suite)\n - [Ignore Response Fields](#ignore-response-fields)\n - [Override ExecutionContext](#override-executioncontext)\n - [Generic Response Comparison](#response-comparison)\n - [Add your Database](#add-your-database)\n\n### TestConfig\nConfiguration for test execution and *rest* calls:\n\n```Scala\nConfig(\"base-host\", 80, \"ref-host\", 80)\n  .repetitions(100)\n  .parallelism(3)\n  .stopOnFailure(false)\n  .shownDiffsLimit(10)\n```\n\n**Mandatory**:\n - `baseHost`: host address of the old/original service\n - `basePort`: port of the old/original service\n - `refactoredHost`: host address of the new/refactored service\n - `refactoredPort`: port of the new/refactored service\n\n**Additional settings**:\n - `repetitions`: [default = 1] how many requests will be created (repeat this check)\n - `parallelism`: [default = 1] how many requests can be ran in parallel\n - `stopOnFailure`: [default = true] if set to `false` the test will continue in the presence of a difference\n - `shownDiffsLimit`: [default = 1] how many diffs are shown\n\n### Providers\nProviders provide a collection of elements of some type `A` for later usage with [data selectors](#data-selector). They have to be tagged (with `Symbol`s) when passed to a test case:\n\n```Scala\nval providers = Providers ~ ('tag0, prov0) ~ ('tag1, prov1)\n```\n\n#### Static\nProvides data from a static sequence:\n\n```Scala\nprovide[User].static(User(\"foo\"), User(\"bar\"))\n```\n\n#### Random\nProvides data from a random generator in a range of `min` / `max`:\n\n```Scala\nprovide[Long].random(0, 100)\n```\n\n#### Database\nProvides data from a Database query:\n\n```Scala\n// provide a query (add a LIMIT as all data is eagerly loaded)!\nprovide[Long].database(\"select id from users limit 100\", db)\n\n// or randomly select 100 elements\nprovide[Long].database.random(\"users\", \"id\", 100, db)\n```\n\n##### Database\nCurrently **artie** provides you with `mysql`, `postgres` and `h2` which can be used like this:\n\n```Scala\nval db0 = mysql(\u003chost\u003e, \u003cuser\u003e, \u003cpassword\u003e)\nval db1 = postgres(\u003chost\u003e, \u003cuser\u003e, \u003cpassword\u003e)\nval db2 = h2(\u003chost\u003e, \u003cuser\u003e, \u003cpassword\u003e)\n```\n\n### Read REST responses\nYou need to tell **artie** how to read the json response sent by your service. To do so you have to create an instance of `Read` for that type:\n\n```Scala\n// manually for every type\nval userRead = new Read[User] {\n  def apply(json: String): Either[String, User] = ???\n}\n\n// or by reusing your mappings from some json-frameworks\nobject PlayJsonToRead {\n\n  def read[U](implicit reads: play.api.libs.json.Reads[U]): Read[U] = new Read[U] {\n    def apply(json: String): Either[String, U] = \n      Json.fromJson[U](Json.parse(json)) match {\n        case JsSuccess(u, _) =\u003e Right(u)\n        case JsError(errors) =\u003e Left(errors.mkString(\"\\n\"))\n      }\n  }\n}\n```\n\nNow we can build a test case by calling `check`:\n\n```Scala\n// r := Random instance\n// p := list of all our tagged providers\ncheck(\"my endpoint\", providers, config, read[User]) { implicit r =\u003e p =\u003e\n  ???\n}\n```\n\nBut wait, how do we get data out of our provider instance `p` to build requests?\n\n### Data Selector\nYou can select data from some provider `'tag` as shown below:\n\n```Scala\nimplicit r =\u003e p =\u003e \n  select('tag, p).next // single element\n  select('tag, p).nextOpt // single element which can be `Some` or `None`\n  select('tag, p).nextSeq(10) // sequence of elements of length 10\n  select('tag, p).nextSet(10) // set of elements of maximum size 10\n```\n\nIf you try to access a provider which isn't part of `p` the compiler will tell you.\n\n### Request Builder\nYou can create:\n - *get*\n - *put*\n - *post*\n - *delete*\n\nrequests by calling:\n\n```Scala\nget(\"http://my.service/test\")\n\npost(\"http://my.service/test\", contentO = Some(\"\"\"{\"id\":0}\"\"\"))\n```\n\n#### Query Parameter and Headers\nIf you need query parameters or headers use:\n\n```Scala\nval p = Params \u003c\u0026\u003e (\"a\", 1) \u003c\u0026\u003e (\"b\", Some(0)) \u003c\u0026\u003e (\"c\", Seq(1, 2, 3))\nval h = Headers \u003c\u0026\u003e (\"Content-Type\", \"application/json\")\n\nget(\"http://my.service/test\", params = p, headers = h)\n```\n\n### Test Suite\nYou don't want to execute all your specs by hand? Then add a `RefactoringSuite`:\n\n```Scala\nobject MySuite extends RefactoringSuite {\n  \n  val specs = FirstSpec :: SecondSpec :: Nil\n}\n```\n\nThis will execute all your `RefactoringSpec`s you add to `specs` in sequence.\n\n### Ignore Response Fields\nSometimes it is necessary to ignore some response fields (eg. timestamp). If you don't want to rewrite your\njson mapping you can provide a `IgnoreFields` instance:\n\n```\nfinal case class Log(msg: String, time: Long)\n\nimplicit val logIgnore = IgnoreFields[Log].ignore('time)\n   \ncheck(\"log-endpoint\", providers, conf, read[Log]) { ...}\n```\n\nThe `Symbol` has to be equal to the field name. If you write something which doesn't exists in your `case class`\nthe compiler will tell you.\n\n### Override ExecutionContext\n**artie** uses `ExecutionContext.global` by default, but if you need a specific context you can override it with:\n\n```Scala\nobject MyRefactoring extends RefactoringSpec {\n  override implicit val executionContext = myContext\n  \n  ...\n}\n```\n\n### Response Comparison\nResponse comparison is done by creating a list of field-value pairs ([LabelledGenerics](https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#generic-representation-of-sealed-families-of-case-classes)) from your responses of class `R` \nand comparing each field:\n\n```Scala\n(old: R, refact: R) =\u003e (old: FieldValues, refact: FieldValues) =\u003e Seq[Diff]\n```\n\nThe result is a sequence of `Diff` with each diff providing the field name and:\n  - the two original values,\n  - a set of diffs of the two values.\n\nCurrently the framework is able to provide detailed comparison results (fields with values) for:\n  - simple case classes (`case class User(id: Long, name: String`)\n  - nested case classes (`case class Friendship(base: User, friend: User)`)\n  - sequences, sets and arrays within case classes (`case class Group(members: Set[User])`)\n  - maps within case classes (`case class UserTopic(topics: Map[Topic, User])`)\n  - sequences and arrays of case classes (`Array[User]`)\n  - maps of case classses (`Map[Int, Group]`)\n\nEverythings else will be compare by `!=` and completely reported on failure.\n\nIf you need something else take a look [here](https://github.com/pheymann/artie/blob/master/core/src/main/scala/artie/GenericDiff.scala) to get an idea how to implement it.\n\n### Add your Database\nYou can add your Database as easy as this:\n\n```Scala\ntrait Mysql extends Database {\n\n  val driver = \"com.mysql.jdbc.Driver\"\n\n  def qualifiedHost = \"jdbc:mysql://\" + host\n\n  def randomQuery(table: String, column: String, limit: Int): String =\n    s\"\"\"SELECT DISTINCT t.$column\n       |FROM $table AS t\n       |ORDER BY RAND()\n       |LIMIT $limit\n       |\"\"\".stripMargin  \n}\n\nobject Mysql {\n\n  def apply(_host: String, _user: String, _password: String) = new Mysql {\n    val host = _host\n    val user = _user\n    val password = _password\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpheymann%2Fartie","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpheymann%2Fartie","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpheymann%2Fartie/lists"}