restspec 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +21 -0
- data/.gitignore +23 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +188 -0
- data/ROADMAP.md +11 -0
- data/Rakefile +20 -0
- data/bin/restspec +54 -0
- data/bin/templates/Gemfile +3 -0
- data/bin/templates/custom_macros.rb +3 -0
- data/bin/templates/restspec_config.rb +10 -0
- data/bin/templates/spec_helper.rb +19 -0
- data/docs/endpoints.md +200 -0
- data/docs/helpers.md +40 -0
- data/docs/macros.md +140 -0
- data/docs/matchers.md +38 -0
- data/docs/schemas.md +28 -0
- data/docs/tutorial.md +477 -0
- data/docs/types.md +134 -0
- data/examples/store-api-tests/.rspec +3 -0
- data/examples/store-api-tests/Gemfile +4 -0
- data/examples/store-api-tests/Gemfile.lock +70 -0
- data/examples/store-api-tests/spec/api/category_spec.rb +23 -0
- data/examples/store-api-tests/spec/api/product_spec.rb +55 -0
- data/examples/store-api-tests/spec/api/restspec/endpoints.rb +39 -0
- data/examples/store-api-tests/spec/api/restspec/requirements.rb +0 -0
- data/examples/store-api-tests/spec/api/restspec/restspec_config.rb +6 -0
- data/examples/store-api-tests/spec/api/restspec/schemas.rb +11 -0
- data/examples/store-api-tests/spec/spec_helper.rb +19 -0
- data/examples/store-api-tests/spec/support/custom_macros.rb +3 -0
- data/examples/store-api-tests/spec/support/custom_matchers.rb +0 -0
- data/examples/store-api/.editorconfig +24 -0
- data/examples/store-api/.rbenv-vars.example +3 -0
- data/examples/store-api/.rspec +4 -0
- data/examples/store-api/.ruby-version +1 -0
- data/examples/store-api/Gemfile +58 -0
- data/examples/store-api/Gemfile.lock +216 -0
- data/examples/store-api/Guardfile +39 -0
- data/examples/store-api/README.md +1 -0
- data/examples/store-api/Rakefile +6 -0
- data/examples/store-api/app/assets/images/.keep +0 -0
- data/examples/store-api/app/assets/javascripts/application.js +16 -0
- data/examples/store-api/app/assets/javascripts/categories.js.coffee +3 -0
- data/examples/store-api/app/assets/javascripts/products.js.coffee +3 -0
- data/examples/store-api/app/assets/stylesheets/application.css +15 -0
- data/examples/store-api/app/assets/stylesheets/categories.css.scss +3 -0
- data/examples/store-api/app/assets/stylesheets/products.css.scss +3 -0
- data/examples/store-api/app/assets/stylesheets/scaffolds.css.scss +69 -0
- data/examples/store-api/app/controllers/application_controller.rb +5 -0
- data/examples/store-api/app/controllers/categories_controller.rb +74 -0
- data/examples/store-api/app/controllers/concerns/.keep +0 -0
- data/examples/store-api/app/controllers/products_controller.rb +74 -0
- data/examples/store-api/app/helpers/application_helper.rb +2 -0
- data/examples/store-api/app/helpers/categories_helper.rb +2 -0
- data/examples/store-api/app/helpers/products_helper.rb +2 -0
- data/examples/store-api/app/mailers/.keep +0 -0
- data/examples/store-api/app/models/.keep +0 -0
- data/examples/store-api/app/models/category.rb +2 -0
- data/examples/store-api/app/models/concerns/.keep +0 -0
- data/examples/store-api/app/models/product.rb +3 -0
- data/examples/store-api/app/views/categories/_form.html.erb +21 -0
- data/examples/store-api/app/views/categories/edit.html.erb +6 -0
- data/examples/store-api/app/views/categories/index.html.erb +25 -0
- data/examples/store-api/app/views/categories/index.json.jbuilder +4 -0
- data/examples/store-api/app/views/categories/new.html.erb +5 -0
- data/examples/store-api/app/views/categories/show.html.erb +9 -0
- data/examples/store-api/app/views/categories/show.json.jbuilder +1 -0
- data/examples/store-api/app/views/layouts/application.html.erb +14 -0
- data/examples/store-api/app/views/products/_form.html.erb +29 -0
- data/examples/store-api/app/views/products/edit.html.erb +6 -0
- data/examples/store-api/app/views/products/index.html.erb +29 -0
- data/examples/store-api/app/views/products/index.json.jbuilder +4 -0
- data/examples/store-api/app/views/products/new.html.erb +5 -0
- data/examples/store-api/app/views/products/show.html.erb +19 -0
- data/examples/store-api/app/views/products/show.json.jbuilder +6 -0
- data/examples/store-api/bin/bundle +3 -0
- data/examples/store-api/bin/guard +16 -0
- data/examples/store-api/bin/rails +8 -0
- data/examples/store-api/bin/rake +8 -0
- data/examples/store-api/bin/spring +18 -0
- data/examples/store-api/config.ru +4 -0
- data/examples/store-api/config/application.rb +30 -0
- data/examples/store-api/config/boot.rb +4 -0
- data/examples/store-api/config/database.yml +25 -0
- data/examples/store-api/config/environment.rb +5 -0
- data/examples/store-api/config/environments/development.rb +37 -0
- data/examples/store-api/config/environments/production.rb +78 -0
- data/examples/store-api/config/environments/test.rb +39 -0
- data/examples/store-api/config/initializers/assets.rb +8 -0
- data/examples/store-api/config/initializers/backtrace_silencers.rb +7 -0
- data/examples/store-api/config/initializers/cookies_serializer.rb +3 -0
- data/examples/store-api/config/initializers/filter_parameter_logging.rb +4 -0
- data/examples/store-api/config/initializers/inflections.rb +16 -0
- data/examples/store-api/config/initializers/mime_types.rb +4 -0
- data/examples/store-api/config/initializers/session_store.rb +3 -0
- data/examples/store-api/config/initializers/wrap_parameters.rb +14 -0
- data/examples/store-api/config/locales/en.yml +23 -0
- data/examples/store-api/config/routes.rb +59 -0
- data/examples/store-api/config/secrets.yml +22 -0
- data/examples/store-api/db/migrate/20141205154816_create_products.rb +11 -0
- data/examples/store-api/db/migrate/20141205171104_create_categories.rb +9 -0
- data/examples/store-api/db/migrate/20141205171140_add_category_id_to_products.rb +5 -0
- data/examples/store-api/db/schema.rb +31 -0
- data/examples/store-api/db/seeds.rb +7 -0
- data/examples/store-api/lib/assets/.keep +0 -0
- data/examples/store-api/lib/tasks/.keep +0 -0
- data/examples/store-api/log/.keep +0 -0
- data/examples/store-api/public/404.html +67 -0
- data/examples/store-api/public/422.html +67 -0
- data/examples/store-api/public/500.html +66 -0
- data/examples/store-api/public/favicon.ico +0 -0
- data/examples/store-api/public/robots.txt +5 -0
- data/examples/store-api/spec/controllers/categories_controller_spec.rb +159 -0
- data/examples/store-api/spec/controllers/products_controller_spec.rb +159 -0
- data/examples/store-api/spec/factories/categories.rb +6 -0
- data/examples/store-api/spec/factories/products.rb +8 -0
- data/examples/store-api/spec/helpers/categories_helper_spec.rb +15 -0
- data/examples/store-api/spec/helpers/products_helper_spec.rb +15 -0
- data/examples/store-api/spec/models/category_spec.rb +5 -0
- data/examples/store-api/spec/models/product_spec.rb +5 -0
- data/examples/store-api/spec/rails_helper.rb +50 -0
- data/examples/store-api/spec/requests/categories_spec.rb +10 -0
- data/examples/store-api/spec/requests/products_spec.rb +10 -0
- data/examples/store-api/spec/routing/categories_routing_spec.rb +35 -0
- data/examples/store-api/spec/routing/products_routing_spec.rb +35 -0
- data/examples/store-api/spec/spec_helper.rb +85 -0
- data/examples/store-api/spec/views/categories/edit.html.erb_spec.rb +18 -0
- data/examples/store-api/spec/views/categories/index.html.erb_spec.rb +19 -0
- data/examples/store-api/spec/views/categories/new.html.erb_spec.rb +18 -0
- data/examples/store-api/spec/views/categories/show.html.erb_spec.rb +14 -0
- data/examples/store-api/spec/views/products/edit.html.erb_spec.rb +24 -0
- data/examples/store-api/spec/views/products/index.html.erb_spec.rb +25 -0
- data/examples/store-api/spec/views/products/new.html.erb_spec.rb +24 -0
- data/examples/store-api/spec/views/products/show.html.erb_spec.rb +18 -0
- data/examples/store-api/vendor/assets/javascripts/.keep +0 -0
- data/examples/store-api/vendor/assets/stylesheets/.keep +0 -0
- data/lib/restspec.rb +38 -0
- data/lib/restspec/configuration.rb +43 -0
- data/lib/restspec/endpoints/dsl.rb +142 -0
- data/lib/restspec/endpoints/endpoint.rb +135 -0
- data/lib/restspec/endpoints/namespace.rb +89 -0
- data/lib/restspec/endpoints/network.rb +39 -0
- data/lib/restspec/endpoints/request.rb +11 -0
- data/lib/restspec/endpoints/response.rb +53 -0
- data/lib/restspec/requirements/dsl.rb +10 -0
- data/lib/restspec/requirements/requirement.rb +59 -0
- data/lib/restspec/rspec/api_helpers.rb +64 -0
- data/lib/restspec/rspec/api_macros.rb +126 -0
- data/lib/restspec/rspec/extras.rb +2 -0
- data/lib/restspec/rspec/matchers/api_matchers.rb +6 -0
- data/lib/restspec/rspec/matchers/be_like_schema.rb +18 -0
- data/lib/restspec/rspec/matchers/be_like_schema_array.rb +18 -0
- data/lib/restspec/rspec/matchers/have_header.rb +47 -0
- data/lib/restspec/rspec/matchers/have_status.rb +17 -0
- data/lib/restspec/rspec/matchers/include_where.rb +14 -0
- data/lib/restspec/rspec/shared_examples.rb +12 -0
- data/lib/restspec/schema/attribute.rb +31 -0
- data/lib/restspec/schema/attribute_example.rb +21 -0
- data/lib/restspec/schema/checker.rb +73 -0
- data/lib/restspec/schema/dsl.rb +36 -0
- data/lib/restspec/schema/schema.rb +21 -0
- data/lib/restspec/schema/schema_example.rb +28 -0
- data/lib/restspec/schema/types.rb +35 -0
- data/lib/restspec/schema/types/array_type.rb +34 -0
- data/lib/restspec/schema/types/basic_type.rb +35 -0
- data/lib/restspec/schema/types/boolean_type.rb +11 -0
- data/lib/restspec/schema/types/decimal_string_type.rb +32 -0
- data/lib/restspec/schema/types/decimal_type.rb +14 -0
- data/lib/restspec/schema/types/embedded_schema_type.rb +28 -0
- data/lib/restspec/schema/types/hash_type.rb +25 -0
- data/lib/restspec/schema/types/integer_type.rb +11 -0
- data/lib/restspec/schema/types/null_type.rb +11 -0
- data/lib/restspec/schema/types/one_of_type.rb +21 -0
- data/lib/restspec/schema/types/schema_id_type.rb +88 -0
- data/lib/restspec/schema/types/string_type.rb +11 -0
- data/lib/restspec/shortcuts.rb +8 -0
- data/lib/restspec/stores/endpoint_store.rb +25 -0
- data/lib/restspec/stores/namespace_store.rb +20 -0
- data/lib/restspec/stores/schema_store.rb +19 -0
- data/lib/restspec/values/status_code.rb +13 -0
- data/lib/restspec/values/super_hash.rb +12 -0
- data/lib/restspec/version.rb +3 -0
- data/restspec.gemspec +37 -0
- data/spec/restspec/endpoints/dsl_spec.rb +269 -0
- data/spec/restspec/endpoints/endpoint_spec.rb +146 -0
- data/spec/restspec/endpoints/namespace_spec.rb +143 -0
- data/spec/restspec/endpoints/response_spec.rb +49 -0
- data/spec/restspec/schema/attribute_example_spec.rb +35 -0
- data/spec/restspec/schema/dsl_spec.rb +78 -0
- data/spec/restspec/schema/schema_example_spec.rb +40 -0
- data/spec/restspec/schema/schema_spec.rb +11 -0
- data/spec/restspec/schema/types/array_type_spec.rb +56 -0
- data/spec/restspec/schema/types/basic_type_spec.rb +62 -0
- data/spec/restspec/schema/types/boolean_type_spec.rb +26 -0
- data/spec/restspec/schema/types/null_type_spec.rb +25 -0
- data/spec/restspec/schema/types/string_type_spec.rb +26 -0
- data/spec/restspec/values/status_code_spec.rb +13 -0
- data/spec/spec_helper.rb +23 -0
- metadata +484 -0
data/docs/helpers.md
ADDED
@@ -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.
|
data/docs/macros.md
ADDED
@@ -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.
|
data/docs/matchers.md
ADDED
@@ -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
|
+
|
data/docs/schemas.md
ADDED
@@ -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).
|
data/docs/tutorial.md
ADDED
@@ -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.
|