restspec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (202) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +21 -0
  3. data/.gitignore +23 -0
  4. data/.rspec +4 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +6 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +188 -0
  9. data/ROADMAP.md +11 -0
  10. data/Rakefile +20 -0
  11. data/bin/restspec +54 -0
  12. data/bin/templates/Gemfile +3 -0
  13. data/bin/templates/custom_macros.rb +3 -0
  14. data/bin/templates/restspec_config.rb +10 -0
  15. data/bin/templates/spec_helper.rb +19 -0
  16. data/docs/endpoints.md +200 -0
  17. data/docs/helpers.md +40 -0
  18. data/docs/macros.md +140 -0
  19. data/docs/matchers.md +38 -0
  20. data/docs/schemas.md +28 -0
  21. data/docs/tutorial.md +477 -0
  22. data/docs/types.md +134 -0
  23. data/examples/store-api-tests/.rspec +3 -0
  24. data/examples/store-api-tests/Gemfile +4 -0
  25. data/examples/store-api-tests/Gemfile.lock +70 -0
  26. data/examples/store-api-tests/spec/api/category_spec.rb +23 -0
  27. data/examples/store-api-tests/spec/api/product_spec.rb +55 -0
  28. data/examples/store-api-tests/spec/api/restspec/endpoints.rb +39 -0
  29. data/examples/store-api-tests/spec/api/restspec/requirements.rb +0 -0
  30. data/examples/store-api-tests/spec/api/restspec/restspec_config.rb +6 -0
  31. data/examples/store-api-tests/spec/api/restspec/schemas.rb +11 -0
  32. data/examples/store-api-tests/spec/spec_helper.rb +19 -0
  33. data/examples/store-api-tests/spec/support/custom_macros.rb +3 -0
  34. data/examples/store-api-tests/spec/support/custom_matchers.rb +0 -0
  35. data/examples/store-api/.editorconfig +24 -0
  36. data/examples/store-api/.rbenv-vars.example +3 -0
  37. data/examples/store-api/.rspec +4 -0
  38. data/examples/store-api/.ruby-version +1 -0
  39. data/examples/store-api/Gemfile +58 -0
  40. data/examples/store-api/Gemfile.lock +216 -0
  41. data/examples/store-api/Guardfile +39 -0
  42. data/examples/store-api/README.md +1 -0
  43. data/examples/store-api/Rakefile +6 -0
  44. data/examples/store-api/app/assets/images/.keep +0 -0
  45. data/examples/store-api/app/assets/javascripts/application.js +16 -0
  46. data/examples/store-api/app/assets/javascripts/categories.js.coffee +3 -0
  47. data/examples/store-api/app/assets/javascripts/products.js.coffee +3 -0
  48. data/examples/store-api/app/assets/stylesheets/application.css +15 -0
  49. data/examples/store-api/app/assets/stylesheets/categories.css.scss +3 -0
  50. data/examples/store-api/app/assets/stylesheets/products.css.scss +3 -0
  51. data/examples/store-api/app/assets/stylesheets/scaffolds.css.scss +69 -0
  52. data/examples/store-api/app/controllers/application_controller.rb +5 -0
  53. data/examples/store-api/app/controllers/categories_controller.rb +74 -0
  54. data/examples/store-api/app/controllers/concerns/.keep +0 -0
  55. data/examples/store-api/app/controllers/products_controller.rb +74 -0
  56. data/examples/store-api/app/helpers/application_helper.rb +2 -0
  57. data/examples/store-api/app/helpers/categories_helper.rb +2 -0
  58. data/examples/store-api/app/helpers/products_helper.rb +2 -0
  59. data/examples/store-api/app/mailers/.keep +0 -0
  60. data/examples/store-api/app/models/.keep +0 -0
  61. data/examples/store-api/app/models/category.rb +2 -0
  62. data/examples/store-api/app/models/concerns/.keep +0 -0
  63. data/examples/store-api/app/models/product.rb +3 -0
  64. data/examples/store-api/app/views/categories/_form.html.erb +21 -0
  65. data/examples/store-api/app/views/categories/edit.html.erb +6 -0
  66. data/examples/store-api/app/views/categories/index.html.erb +25 -0
  67. data/examples/store-api/app/views/categories/index.json.jbuilder +4 -0
  68. data/examples/store-api/app/views/categories/new.html.erb +5 -0
  69. data/examples/store-api/app/views/categories/show.html.erb +9 -0
  70. data/examples/store-api/app/views/categories/show.json.jbuilder +1 -0
  71. data/examples/store-api/app/views/layouts/application.html.erb +14 -0
  72. data/examples/store-api/app/views/products/_form.html.erb +29 -0
  73. data/examples/store-api/app/views/products/edit.html.erb +6 -0
  74. data/examples/store-api/app/views/products/index.html.erb +29 -0
  75. data/examples/store-api/app/views/products/index.json.jbuilder +4 -0
  76. data/examples/store-api/app/views/products/new.html.erb +5 -0
  77. data/examples/store-api/app/views/products/show.html.erb +19 -0
  78. data/examples/store-api/app/views/products/show.json.jbuilder +6 -0
  79. data/examples/store-api/bin/bundle +3 -0
  80. data/examples/store-api/bin/guard +16 -0
  81. data/examples/store-api/bin/rails +8 -0
  82. data/examples/store-api/bin/rake +8 -0
  83. data/examples/store-api/bin/spring +18 -0
  84. data/examples/store-api/config.ru +4 -0
  85. data/examples/store-api/config/application.rb +30 -0
  86. data/examples/store-api/config/boot.rb +4 -0
  87. data/examples/store-api/config/database.yml +25 -0
  88. data/examples/store-api/config/environment.rb +5 -0
  89. data/examples/store-api/config/environments/development.rb +37 -0
  90. data/examples/store-api/config/environments/production.rb +78 -0
  91. data/examples/store-api/config/environments/test.rb +39 -0
  92. data/examples/store-api/config/initializers/assets.rb +8 -0
  93. data/examples/store-api/config/initializers/backtrace_silencers.rb +7 -0
  94. data/examples/store-api/config/initializers/cookies_serializer.rb +3 -0
  95. data/examples/store-api/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/examples/store-api/config/initializers/inflections.rb +16 -0
  97. data/examples/store-api/config/initializers/mime_types.rb +4 -0
  98. data/examples/store-api/config/initializers/session_store.rb +3 -0
  99. data/examples/store-api/config/initializers/wrap_parameters.rb +14 -0
  100. data/examples/store-api/config/locales/en.yml +23 -0
  101. data/examples/store-api/config/routes.rb +59 -0
  102. data/examples/store-api/config/secrets.yml +22 -0
  103. data/examples/store-api/db/migrate/20141205154816_create_products.rb +11 -0
  104. data/examples/store-api/db/migrate/20141205171104_create_categories.rb +9 -0
  105. data/examples/store-api/db/migrate/20141205171140_add_category_id_to_products.rb +5 -0
  106. data/examples/store-api/db/schema.rb +31 -0
  107. data/examples/store-api/db/seeds.rb +7 -0
  108. data/examples/store-api/lib/assets/.keep +0 -0
  109. data/examples/store-api/lib/tasks/.keep +0 -0
  110. data/examples/store-api/log/.keep +0 -0
  111. data/examples/store-api/public/404.html +67 -0
  112. data/examples/store-api/public/422.html +67 -0
  113. data/examples/store-api/public/500.html +66 -0
  114. data/examples/store-api/public/favicon.ico +0 -0
  115. data/examples/store-api/public/robots.txt +5 -0
  116. data/examples/store-api/spec/controllers/categories_controller_spec.rb +159 -0
  117. data/examples/store-api/spec/controllers/products_controller_spec.rb +159 -0
  118. data/examples/store-api/spec/factories/categories.rb +6 -0
  119. data/examples/store-api/spec/factories/products.rb +8 -0
  120. data/examples/store-api/spec/helpers/categories_helper_spec.rb +15 -0
  121. data/examples/store-api/spec/helpers/products_helper_spec.rb +15 -0
  122. data/examples/store-api/spec/models/category_spec.rb +5 -0
  123. data/examples/store-api/spec/models/product_spec.rb +5 -0
  124. data/examples/store-api/spec/rails_helper.rb +50 -0
  125. data/examples/store-api/spec/requests/categories_spec.rb +10 -0
  126. data/examples/store-api/spec/requests/products_spec.rb +10 -0
  127. data/examples/store-api/spec/routing/categories_routing_spec.rb +35 -0
  128. data/examples/store-api/spec/routing/products_routing_spec.rb +35 -0
  129. data/examples/store-api/spec/spec_helper.rb +85 -0
  130. data/examples/store-api/spec/views/categories/edit.html.erb_spec.rb +18 -0
  131. data/examples/store-api/spec/views/categories/index.html.erb_spec.rb +19 -0
  132. data/examples/store-api/spec/views/categories/new.html.erb_spec.rb +18 -0
  133. data/examples/store-api/spec/views/categories/show.html.erb_spec.rb +14 -0
  134. data/examples/store-api/spec/views/products/edit.html.erb_spec.rb +24 -0
  135. data/examples/store-api/spec/views/products/index.html.erb_spec.rb +25 -0
  136. data/examples/store-api/spec/views/products/new.html.erb_spec.rb +24 -0
  137. data/examples/store-api/spec/views/products/show.html.erb_spec.rb +18 -0
  138. data/examples/store-api/vendor/assets/javascripts/.keep +0 -0
  139. data/examples/store-api/vendor/assets/stylesheets/.keep +0 -0
  140. data/lib/restspec.rb +38 -0
  141. data/lib/restspec/configuration.rb +43 -0
  142. data/lib/restspec/endpoints/dsl.rb +142 -0
  143. data/lib/restspec/endpoints/endpoint.rb +135 -0
  144. data/lib/restspec/endpoints/namespace.rb +89 -0
  145. data/lib/restspec/endpoints/network.rb +39 -0
  146. data/lib/restspec/endpoints/request.rb +11 -0
  147. data/lib/restspec/endpoints/response.rb +53 -0
  148. data/lib/restspec/requirements/dsl.rb +10 -0
  149. data/lib/restspec/requirements/requirement.rb +59 -0
  150. data/lib/restspec/rspec/api_helpers.rb +64 -0
  151. data/lib/restspec/rspec/api_macros.rb +126 -0
  152. data/lib/restspec/rspec/extras.rb +2 -0
  153. data/lib/restspec/rspec/matchers/api_matchers.rb +6 -0
  154. data/lib/restspec/rspec/matchers/be_like_schema.rb +18 -0
  155. data/lib/restspec/rspec/matchers/be_like_schema_array.rb +18 -0
  156. data/lib/restspec/rspec/matchers/have_header.rb +47 -0
  157. data/lib/restspec/rspec/matchers/have_status.rb +17 -0
  158. data/lib/restspec/rspec/matchers/include_where.rb +14 -0
  159. data/lib/restspec/rspec/shared_examples.rb +12 -0
  160. data/lib/restspec/schema/attribute.rb +31 -0
  161. data/lib/restspec/schema/attribute_example.rb +21 -0
  162. data/lib/restspec/schema/checker.rb +73 -0
  163. data/lib/restspec/schema/dsl.rb +36 -0
  164. data/lib/restspec/schema/schema.rb +21 -0
  165. data/lib/restspec/schema/schema_example.rb +28 -0
  166. data/lib/restspec/schema/types.rb +35 -0
  167. data/lib/restspec/schema/types/array_type.rb +34 -0
  168. data/lib/restspec/schema/types/basic_type.rb +35 -0
  169. data/lib/restspec/schema/types/boolean_type.rb +11 -0
  170. data/lib/restspec/schema/types/decimal_string_type.rb +32 -0
  171. data/lib/restspec/schema/types/decimal_type.rb +14 -0
  172. data/lib/restspec/schema/types/embedded_schema_type.rb +28 -0
  173. data/lib/restspec/schema/types/hash_type.rb +25 -0
  174. data/lib/restspec/schema/types/integer_type.rb +11 -0
  175. data/lib/restspec/schema/types/null_type.rb +11 -0
  176. data/lib/restspec/schema/types/one_of_type.rb +21 -0
  177. data/lib/restspec/schema/types/schema_id_type.rb +88 -0
  178. data/lib/restspec/schema/types/string_type.rb +11 -0
  179. data/lib/restspec/shortcuts.rb +8 -0
  180. data/lib/restspec/stores/endpoint_store.rb +25 -0
  181. data/lib/restspec/stores/namespace_store.rb +20 -0
  182. data/lib/restspec/stores/schema_store.rb +19 -0
  183. data/lib/restspec/values/status_code.rb +13 -0
  184. data/lib/restspec/values/super_hash.rb +12 -0
  185. data/lib/restspec/version.rb +3 -0
  186. data/restspec.gemspec +37 -0
  187. data/spec/restspec/endpoints/dsl_spec.rb +269 -0
  188. data/spec/restspec/endpoints/endpoint_spec.rb +146 -0
  189. data/spec/restspec/endpoints/namespace_spec.rb +143 -0
  190. data/spec/restspec/endpoints/response_spec.rb +49 -0
  191. data/spec/restspec/schema/attribute_example_spec.rb +35 -0
  192. data/spec/restspec/schema/dsl_spec.rb +78 -0
  193. data/spec/restspec/schema/schema_example_spec.rb +40 -0
  194. data/spec/restspec/schema/schema_spec.rb +11 -0
  195. data/spec/restspec/schema/types/array_type_spec.rb +56 -0
  196. data/spec/restspec/schema/types/basic_type_spec.rb +62 -0
  197. data/spec/restspec/schema/types/boolean_type_spec.rb +26 -0
  198. data/spec/restspec/schema/types/null_type_spec.rb +25 -0
  199. data/spec/restspec/schema/types/string_type_spec.rb +26 -0
  200. data/spec/restspec/values/status_code_spec.rb +13 -0
  201. data/spec/spec_helper.rb +23 -0
  202. metadata +484 -0
@@ -0,0 +1,40 @@
1
+ # Helpers
2
+
3
+ ## read_endpoint
4
+
5
+ It receives an endpoint name (in the form `namespace/endpoint`), and calls the endpoint returning the body of the call. It can receive the following options:
6
+
7
+ - **url_params:** (a hash)
8
+ - **body:** (a hash)
9
+ - **query_params:** (a hash)
10
+ - **merge_example_params:** (a boolean) This is useful to decide if we have to use the params (query and url ones) defined in an outer block and merge the with the params pased as arguments. By default is true)
11
+
12
+ ```ruby
13
+ RSpec.describe :books do
14
+ endpoint 'books/create' do
15
+ payload { schema_example(:book) }
16
+
17
+ test do
18
+ it { should have_status(201) }
19
+
20
+ it "actually creates a book" do
21
+ book = read_endpoint('books/show', url_params: { id: body.id })
22
+ expect(book).to be_present
23
+ expect(book.id).to be_present
24
+ end
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ ## call_endpoint
31
+
32
+ It is the same as `read_endpoint` but instead of return the `body` of the response, it returns the response itself. It can be done to just call the response or to get the headers and status of the response.
33
+
34
+ ## execute_endpoint!
35
+
36
+ This method re-executes the current endpoint. It exists just for extreme cases.
37
+
38
+ ## schema_example
39
+
40
+ It receives a schema name and returns an example generated from the schema. Usually, the result is a hash. It should be used for payloads.
@@ -0,0 +1,140 @@
1
+ # Macros
2
+
3
+ ## endpoint
4
+
5
+ The `endpoint` macro generates a describe block that saves inside itself the reference of an endpoint, finding it by his full name.
6
+
7
+ ```ruby
8
+ RSpec.describe :books do
9
+ endpoint 'books/create' do
10
+ end
11
+ end
12
+ ```
13
+
14
+ It can also receive the option `:implicit_test` to set a `test` block inside it automatically:
15
+
16
+ ```ruby
17
+ RSpec.describe :books do
18
+ endpoint 'books/create', implicit_test: true do
19
+ it { should have_status(:ok) }
20
+ end
21
+ end
22
+ ```
23
+
24
+ It can be converted in an test aware of a resource using the `resource` endpoint. This will change the endpoint execution flow to first retrieve a resource to keep track of the state of the resource before and after the endpoint execution:
25
+
26
+ ```ruby
27
+ RSpec.describe :books do
28
+ endpoint 'books/update', resource: 'books/show' do
29
+ end
30
+ end
31
+ ```
32
+
33
+ ## test
34
+
35
+ `test` creates a context where the endpoint attached in the parent `endpoint` will be executed only once before the tests run. In this way, we can have a lot of tests against a single execution. It receives an optional name parameter and a set of options that are passed to the context (to enable the rspec tags support). The name, when no supplied, will be `"the happy path"` to express that the `test` macro is intended to specify different endpoint executions, typically once for each use case.
36
+
37
+
38
+ ```ruby
39
+ RSpec.describe :books do
40
+ endpoint 'books/index' do
41
+ test do
42
+ it { should have_status(200) }
43
+ it { should be_like_schema_array(:book) }
44
+ it { should have_header('Cache-Control').equals('private, max-age=0, no-cache') }
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ ## payload
51
+
52
+ The `payload` macro works inside a `test` macro allowing to specify the payload of the request. It can receive a hash or a block that returns a hash for more flexibility. Usually, the payload should use the `schema_example` helper.
53
+
54
+ ```ruby
55
+ RSpec.describe :books do
56
+ endpoint 'books/create' do
57
+ payload title: 'Title', published_at: 2.days.ago
58
+
59
+ test do
60
+ it { should have_status(201) }
61
+ end
62
+ end
63
+ end
64
+ ```
65
+
66
+ ## url_params
67
+
68
+ The `url_params` macro specifies url parameters to use in the endpoint call. It can be a hash or a block that returns a hash. If some of the keys of the hash is a lambda, the lambda will be executed.
69
+
70
+ ```ruby
71
+ RSpec.describe :books do
72
+ endpoint 'books/show' do
73
+ # let's imagine: /library/:library_id/books/:id
74
+ url_params do
75
+ # Typically, you will use the `read_endpoint` helper to get
76
+ # some data instead of just hardcoding numbers
77
+ {
78
+ id: ->{ 15 },
79
+ library_id: 10
80
+ }
81
+ end
82
+
83
+ test do
84
+ it { should have_status(200) }
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
90
+ ## query_params
91
+
92
+ The `query_params` macro specifies query parameters to use in the endpoint call. It can be a hash or a block that returns a hash. It works exactly as the `payload` method.
93
+
94
+ ## within_response
95
+
96
+ This macro changes the `subject` of the tests from the response object to the `response.body` for all the tests inside this block. In this way, you can make assertions against the body of the response itself.
97
+
98
+ ```ruby
99
+ RSpec.describe :books do
100
+ endpoint 'books/index' do
101
+ test do
102
+ before_test do
103
+ 3.times { call_endpoint('books/create', body: schema_example(:book)) }
104
+ end
105
+
106
+ within_response do
107
+ it { should have_at_least(3).items }
108
+ end
109
+ end
110
+ end
111
+ end
112
+ ```
113
+
114
+ ## ensure!
115
+
116
+ This macro will run a requirement defined in the `requirements.rb` file. A requirement can be written like this:
117
+
118
+ ```ruby
119
+ requirement :a_bank_exists do
120
+ execution do
121
+ banks = read_endpoint('banks/index')
122
+
123
+ if banks.blank? || banks.size <= 0
124
+ add_error "We need banks to work!!"
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ And can be asserted like this:
131
+
132
+ ```ruby
133
+ RSpec.describe :accounts do
134
+ endpoint 'accounts/create' do
135
+ ensure! :a_bank_exists
136
+ end
137
+ end
138
+ ```
139
+
140
+ If the requirement has errors at the end of the execution, the error will be thrown and everything else will be canceled.
@@ -0,0 +1,38 @@
1
+ # Matchers
2
+
3
+ ## Have Status
4
+
5
+ Tests if the status is equals to a code of the underscored version of [his representation as symbols as they are found in Rails](http://futureshock-ed.com/2011/03/04/http-status-code-symbols-for-rails/).
6
+
7
+ ```ruby
8
+ it { should have_status(:ok) }
9
+ it { should have_status(200) }
10
+ ```
11
+
12
+ ## Have Header
13
+
14
+ Tests if the response have the specified header.
15
+
16
+ ```ruby
17
+ it { should have_header('Content-Type').equals('application/json') }
18
+ it { should have_header('Content-Type').that_contains('json') }
19
+ it { should have_header('Content-Type').that_matches(/json/) }
20
+ ```
21
+
22
+ ## Be Like Schema and Be Like Schema Array
23
+
24
+ Tests if the response's body obbeys to a format. They use the schema names used when defining schemas. The first parameter defaults to the schema name attached to the current endpoint.
25
+
26
+ ```ruby
27
+ it { should be_like_schema(:book) }
28
+ it { should be_like_schema_array(:book) }
29
+ ```
30
+
31
+ ## Include Where
32
+
33
+ A not-so-important but helpful matcher to test if some condition applies to an array.
34
+
35
+ ```ruby
36
+ it { should include_where -> person { person.email == 'email@example.com' } }
37
+ ```
38
+
@@ -0,0 +1,28 @@
1
+ # Schemas DSL
2
+
3
+ ## Top Level DSL
4
+
5
+ ### schema
6
+
7
+ The `schema` method creates a schema with a name and yields a schema dsl.
8
+
9
+ ```ruby
10
+ schema :book do
11
+ end
12
+ ```
13
+
14
+ ## Schema DSL
15
+
16
+ ### attribute
17
+
18
+ The `attribute` method create an attribute inside a `schema`. Actually, a schema is a collection of attributes.
19
+
20
+ ```ruby
21
+ schema :book do
22
+ attribute :title, string
23
+ end
24
+ ```
25
+
26
+ ### Type methods
27
+
28
+ The type methods are shortcuts to create type declarations that decide how an attribute should generate examples and how the attribute can be validated against a value. For more information for each one of them, please see the [types documentation](https://github.com/platanus/restspec/blob/master/docs/types.md).
@@ -0,0 +1,477 @@
1
+ # Getting Started
2
+
3
+ ### Create a Test API Project
4
+
5
+ ```
6
+ $ restspec my-api-tests --api-prefix=http://my-api-domain/api/v1
7
+ $ cd my-api-tests
8
+ $ tree
9
+
10
+ .
11
+ ├── Gemfile
12
+ ├── Gemfile.lock
13
+ └── spec
14
+ ├── api
15
+ │   └── restspec
16
+ │   ├── endpoints.rb
17
+ │   ├── requirements.rb
18
+ │   ├── schemas.rb
19
+ │   └── restspec_config.rb
20
+ ├── spec_helper.rb
21
+ └── support
22
+ ├── custom_macros.rb
23
+ └── custom_matchers.rb
24
+
25
+ ```
26
+
27
+ If you're familiar with the regular use of RSpec, this initial structure should look normal except from the the files in the `api/restspec` folder.
28
+
29
+ - **endpoints.rb**: This is the place to describe your endpoints. One important thing is that your tests **won't define your endpoints**. In this way, we have a graph of the structure of your api *always* and this can help you to visualize your api structure and create better matchers.
30
+ - **schemas.rb**: The schemas are the shape of the resources you are manipulating in the api. It's a centralized place to put how your data should be.
31
+ - **requirements.rb**: The requirements are validations for the initial state of the api system. Testing that the user you are using exists on the api you are working on, testing that some read-only resources created outside of your control exists, etc.
32
+ - **restspec_config.rb**: This is where the configuration for Restspec resides. Here we will put some important things like headers that live in all the application and the url of the API to test.
33
+
34
+ ### Setup
35
+
36
+ In the file `restspec_config.rb` you can change the basic url to use your api. Find the `Restspec`'s `configure` block and set the proper url:
37
+
38
+ ```ruby
39
+ Restspec.configure do |config|
40
+ config.base_url = 'http://my-api-domain/api/v2'
41
+ # ...
42
+ end
43
+ ```
44
+
45
+ ### Usage
46
+
47
+ Anyway, we will add a first test. For this example, we will assume you have a regular api that consists on categories and products. First, we will add a test for the categories endpoints.
48
+
49
+ $ touch spec/api/categories_spec.rb
50
+
51
+ In here, we will create a regular spec with a `describe` block with the `type` option set to `api`. Without the type, you won't get the matchers and macros that `api`-typed specs have.
52
+
53
+ ```ruby
54
+ RSpec.describe :categories_api, :type => :api do
55
+
56
+ end
57
+ ```
58
+
59
+ We want to begin testing the endpoint that creates a category. As said when describing the `endpoints.rb` file, we need to first define our endpoints in there. So, we will open our `endpoints.rb` file and add the following:
60
+
61
+ ```ruby
62
+ namespace :categories do
63
+ endpoint :create do
64
+ path '/categories'
65
+ method :post
66
+ end
67
+ end
68
+ ```
69
+
70
+ This creates an endpoint called `:create` that lives in the namespace `:categories`. Another way, more succint of define the same endpoint is to use the methods `resource` and `post`:
71
+
72
+ ```ruby
73
+ resource :categories do
74
+ post :create, '/categories'
75
+ end
76
+ ```
77
+
78
+ If we use the `collection` method, we won't need to use the `/categories` argument. We can just do this:
79
+
80
+ ```ruby
81
+ resource :categories do
82
+ collection do
83
+ post :create
84
+ end
85
+ end
86
+ ```
87
+
88
+ With this, we have our endpoint. To test this endpoint, just modify your `categories_spec.rb` file to add an endpoint declaration with a `test` block inside it:
89
+
90
+ ```ruby
91
+ RSpec.describe :categories_api, :type => :api do
92
+ endpoint 'categories/create' do
93
+ test do
94
+ end
95
+ end
96
+ end
97
+ ```
98
+
99
+ One important thing to note is that the endpoint is, under the hood, just a `describe` block attached to the endpoint object we just created before. The `test` method is like a context that executes the endpoint just **once** per `test` declaration, regardless of how many examples we have inside it.
100
+
101
+ Inside the test, we can use the [matchers](TODO) offered by Restspec. For example, we can test that the endpoint returned `created`(201).
102
+
103
+ ```ruby
104
+ RSpec.describe :categories_api, :type => :api do
105
+ endpoint 'categories/create' do
106
+ test do
107
+ it { should have_status(:created) }
108
+ end
109
+ end
110
+ end
111
+ ```
112
+
113
+ The output will be something like this:
114
+
115
+ ```
116
+ categories_api
117
+ [POST create]
118
+ the happy path
119
+ should have status :created
120
+ ```
121
+
122
+ This is: A `test` declaration without a name is considered the *happy path*. Any other `test` block should behave like a context responsible for test another path.
123
+
124
+ If we run this test against a properly implemented api, you won't get 201 because you didn't specify a payload for the POST request. To specify a payload, we use the `payload` macro with a hash or a block that returns a hash:
125
+
126
+ ```ruby
127
+ RSpec.describe :categories_api, :type => :api do
128
+ endpoint 'categories/create' do
129
+ test do
130
+ payload name: 'Super Category'
131
+
132
+ it { should have_status(:created) }
133
+ end
134
+ end
135
+ end
136
+ ```
137
+
138
+ Altought this works, it won't scale with more complex structures and you have to decide what the data should be anytime. To help us with this troubles, we will define **schemas**.
139
+
140
+ Schemas are a representation of your data. They are responsible of two things: schema validation and examples generation. Edit your `schemas.rb` file like this:
141
+
142
+ ```ruby
143
+ schema :category do
144
+ attribute :name, string
145
+ end
146
+ ```
147
+
148
+ This is your first schema and it looks straightforward. The `attribute` method defines an attribute with a name and a type. There are [more types than string](TODO) and they can be composed with other types to express some cases. For example, what if the category's name can be string or `null`?
149
+
150
+ For those cases, you can use the `|` operator, that acts like an `or` for types:
151
+
152
+ ```ruby
153
+ schema :category do
154
+ attribute :name, string | null
155
+ end
156
+ ```
157
+
158
+ You can read more about types in [his detailed section](TODO).
159
+
160
+ With our first schema set, we can just express the payload in the test with the `schema_example` helper:
161
+
162
+ ```ruby
163
+ RSpec.describe :categories_api, :type => :api do
164
+ endpoint 'categories/create' do
165
+ test do
166
+ payload { schema_example :category }
167
+
168
+ it { should have_status(:created) }
169
+ end
170
+ end
171
+ end
172
+ ```
173
+
174
+ And we can test that the response's body obtained is a category using the `be_like_schema` matcher:
175
+
176
+ ```ruby
177
+ RSpec.describe :categories_api, :type => :api do
178
+ endpoint 'categories/create' do
179
+ test do
180
+ payload { schema_example :category }
181
+
182
+ it { should have_status(:created) }
183
+ it { should be_like_schema(:category) }
184
+ end
185
+ end
186
+ end
187
+ ```
188
+
189
+
190
+ Because to define the `categories/create` endpoint we used the `resource` method and because the endpoint is called `categories`, the first argument for `schema_example` and `be_like_schema` will default to `:category`, so it's not really needed.
191
+
192
+ ```ruby
193
+ RSpec.describe :categories_api, :type => :api do
194
+ endpoint 'categories/create' do
195
+ test do
196
+ payload { schema_example :category }
197
+
198
+ it { should have_status(:created) }
199
+ it { should be_like_schema }
200
+ end
201
+ end
202
+ end
203
+ ```
204
+
205
+ Say that you don't want to test only that the endpoint returned 201, but you want to be sure that the resource was *really* created. First, we need an endpoint to show a category in our `endpoints.rb` file:
206
+
207
+ ```ruby
208
+ resource :categories do
209
+ collection do
210
+ post :create
211
+ end
212
+
213
+ member do
214
+ get :show
215
+ end
216
+ end
217
+ ```
218
+
219
+ Because the `:show` endpoint is under a `member` declaration, his url is not only `/categories` but `/categories/:id`. In this way, we described a tipical resource endpoint.
220
+
221
+ We can use a new helper here called `read_endpoint` and use a standard RSpec test to test that the category was really created. We also use the `body` method to get the response's body and the `payload` method to get the payload used.
222
+
223
+ ```ruby
224
+ RSpec.describe :categories_api, :type => :api do
225
+ endpoint 'categories/create' do
226
+ test do
227
+ payload { schema_example }
228
+
229
+ it { should have_status(:created) }
230
+ it { should be_like_schema }
231
+
232
+ it "created a category" do
233
+ category = read_endpoint('categories/show', url_params: { id: body.id })
234
+ expect(category.response).to have_status(:ok)
235
+ expect(category.name).to eq(payload.name)
236
+ end
237
+ end
238
+ end
239
+ end
240
+ ```
241
+
242
+ As you can see, you can manipulate endpoints appart from his tests and this is nice because it allows us to use them for things like we have done before.
243
+
244
+ To test the `categories/show` endpoint, we face a tipical problem of API testing: `categories/show` needs an ID, but that ID depends on the `categories/create` endpoint or `categories/index` if you want to. Anyway, we don't want a callback hell formed from this dependencies.
245
+
246
+ A bad but possible solution would be to use the `url_params` macro to set params from reading the `categories/create` endpoint.
247
+
248
+ ```ruby
249
+ endpoint `categories/show` do
250
+ url_params { { id: read_endpoint('categories/create').id } }
251
+
252
+ test do
253
+ it { should have_status(:ok) }
254
+ it { should be_like_schema }
255
+ end
256
+ end
257
+ ```
258
+
259
+ The drawback to this is that we can be putting more crap than needed onto the api database. Wouldn't it be nice if we can try first to get from some place and then, if we can't get from that other place, create something?
260
+
261
+ In the endpoint, we can define this:
262
+
263
+ ```ruby
264
+ resource :categories do
265
+ collection do
266
+ post :create
267
+ end
268
+
269
+ member do
270
+ url_param(:id) { schema_id(:category) }
271
+ get :show
272
+ end
273
+ end
274
+ ```
275
+
276
+ The `url_param` method applies for all the endpoints inside `member` and they tie a url param (`:id`) to an schema type (we could use `integer` to generate a number). In this case we are using a very powerful type called `schema_id`, that generates examples based on the schema that he uses. It searches for endpoints attached to the `category` schema and looks for the endpoints called `index` and `create` to find the specific id. In this way, because the url parameter `:id` is filled by the example, we don't need to specify it. The test could only be like this:
277
+
278
+ ```ruby
279
+ endpoint `categories/show` do
280
+ test do
281
+ it { should have_status(:ok) }
282
+ it { should be_like_schema }
283
+ end
284
+ end
285
+ ```
286
+
287
+ And that's all. Under the hood, it will try to find the category for the endpoint from his dependencies. `SchemaId` is one example of what the separation between endpoints, schemas and tests can bring to api testing.
288
+
289
+ Anyway, we now may need a test for `categories/index`. It can be as easy as:
290
+
291
+ ```ruby
292
+ # in the endpoints
293
+ resource :categories do
294
+ collection do
295
+ post :create
296
+ get :index
297
+ end
298
+
299
+ member do
300
+ url_param(:id) { schema_id(:category) }
301
+ get :show
302
+ end
303
+ end
304
+
305
+ # in the test
306
+ endpoint `categories/index` do
307
+ test do
308
+ it { should have_status(:ok) }
309
+ it { should be_like_schema_array }
310
+ end
311
+ end
312
+ ```
313
+
314
+ Now, we should add tests for the products of the api. The endpoints should be as simple as the ones in `categories` so we will focus in the schemas. Our products have names, so they are simply like this:
315
+
316
+ ```ruby
317
+ schema :product do
318
+ attribute :name, string
319
+ end
320
+ ```
321
+
322
+ The price usually it's a little bit tricky. Some apis may return a number and some other may return a string with the number. For both cases, we have two types: `decimal` and `decimal_string`. In this case we are going to just be lazy with the API and let the type be one of them.
323
+
324
+ ```ruby
325
+ schema :product do
326
+ attribute :name, string
327
+ attribute :price, decimal | decimal_string
328
+ end
329
+ ```
330
+
331
+ It's important to say that the first type in the operation is the one used when creating examples. In this case, the examples will generate a decimal.
332
+
333
+ The last attribute we need to define is the `category_id`. We should be tempted to use an integer but we have something better. A time ago, we talked about the `schema_id` type while defining how to fill the `id` url param of a member endpoint. Well, we can use it here again:
334
+
335
+ ```ruby
336
+ schema :product do
337
+ attribute :name, string
338
+ attribute :price, decimal | decimal_string
339
+ attribute :category_id, schema_id(:category)
340
+ end
341
+ ```
342
+
343
+ And that's all. `be_like_schema` will try to match the id against one example from the `categories/index` endpoint and `schema_example` will generate a product with an `id` from that same endpoint or will create one using `categories/create`. Actually, this is a very repetitive pattern.
344
+
345
+ Finally, we will add the tests for the product api. Let's create `product_spec.rb` file and put some tests inside:
346
+
347
+ ```ruby
348
+ RSpec.describe :products_api, :type => :api do
349
+ endpoint 'products/create' do
350
+ test do
351
+ it { should have_status(:created) }
352
+ it { should be_like_schema }
353
+
354
+ it "created a product" do
355
+ product = read_endpoint('products/show', url_params: { id: body.id })
356
+ expect(product.response).to have_status(:ok)
357
+ expect(product.id).to eq(payload.id)
358
+ end
359
+ end
360
+ end
361
+
362
+ endpoint 'products/show' do
363
+ test do
364
+ it { should have_status(:ok) }
365
+ it { should be_like_schema }
366
+ end
367
+ end
368
+
369
+ endpoint 'products/index' do
370
+ test do
371
+ it { should have_status(:ok) }
372
+ it { should be_like_schema_array }
373
+ end
374
+ end
375
+ end
376
+ ```
377
+
378
+ Now, we are going to add a `products/update` endpoint that should update the product using a `PUT`. This can be something like this:
379
+
380
+ ```ruby
381
+ # endpoints.rb
382
+ resource :products do
383
+ collection do
384
+ post :create
385
+ get :index
386
+ end
387
+
388
+ member do
389
+ url_param(:id) { schema_id(:product) }
390
+
391
+ get :show
392
+ put :update
393
+ end
394
+ end
395
+
396
+ # product_spec.rb
397
+ endpoint 'products/update' do
398
+ test do
399
+ payload { schema_example }
400
+
401
+ it { should have_status :ok }
402
+ it { should be_like_schema }
403
+
404
+ it "updated the parameters" do
405
+ product = read_endpoint('products/show', url_params: { id: body.id })
406
+ expect(product).to include(payload)
407
+ end
408
+ end
409
+ end
410
+ ```
411
+
412
+ Another way to test the `update` endpoint is to define an endpoint test as a resource related test. We can do it like this:
413
+
414
+ ```ruby
415
+ endpoint 'products/update', resource: 'products/show' do
416
+
417
+ end
418
+ ```
419
+
420
+ The `resource` keyword parameter is the endpoint we will use to load a resource instance. The main difference between a normal endpoint test and a resource one is that you have two resources: one before the request and one after that. The `products/show` endpoint should have an `url_param(:id)` set because because we will only execute it, and, without an `id`, we can't have an idea of what resource get. Well, with this new kind of test, we can do the following:
421
+
422
+ ```ruby
423
+ endpoint 'products/update', resource: 'products/show' do
424
+ payload do
425
+ { name: "#{initial_resource.name}-modified" }
426
+ end
427
+
428
+ test do
429
+ it 'updates the name' do
430
+ expect(final_resource.name).to_not eq(initial_resource.name)
431
+ expect(final_resource.name).to eq(request.payload.name)
432
+ end
433
+
434
+ it 'includes the payload' do
435
+ expect(final_resource).to include(payload)
436
+ end
437
+ end
438
+ end
439
+ ```
440
+
441
+
442
+ A `delete` endpoint should be something like this:
443
+
444
+
445
+ ```ruby
446
+ # endpoints.rb
447
+ resource :products do
448
+ collection do
449
+ post :create
450
+ get :index
451
+ end
452
+
453
+ member do
454
+ url_param(:id) { schema_id(:product) }
455
+
456
+ get :show
457
+ put :update
458
+ delete :destroy
459
+ end
460
+ end
461
+
462
+ # product_spec.rb
463
+ endpoint 'products/destroy' do
464
+ test do
465
+ it { should have_status :no_content }
466
+
467
+ it "deleted the product" do
468
+ get_product = call_endpoint('products/show', url_params: {
469
+ id: url_params.id
470
+ })
471
+ expect(get_product).to have_status(:not_found)
472
+ end
473
+ end
474
+ end
475
+ ```
476
+
477
+ And that's all for this not-so-quick mini-tutorial about the usage of Restspec. We can improve more the code but it depends on you in most cases. You can make whatever you want. The most awesome thing about using RSpec is that, if you think that this tests are not DRY enough, you could simply use RSpec's shared examples, macros, helpers and matchers to make tests as concise as you want.