summaryrefslogtreecommitdiff
path: root/qa/qa/factory/README.md
blob: c56c7c431299811aea6356cb15458bfa0c90be87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# Factory objects in GitLab QA

In GitLab QA we are using factories to create resources.

Factories implementation are primarily done using Browser UI steps, but can also
be done via the API.

## Why do we need that?

We need factory objects because we need to reduce duplication when creating
resources for our QA tests.

## How to properly implement a factory object?

All factories should inherit from [`Factory::Base`](./base.rb).

There is only one mandatory method to implement to define a factory. This is the
`#fabricate!` method, which is used to build a resource via the browser UI.
Note that you should only use [Page objects](../page/README.md) to interact with
a Web page in this method.

Here is an imaginary example:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        attr_accessor :name, :size

        def initialize(name)
          @name = name
        end

        def fabricate!
          Page::Dashboard::Index.perform do |dashboard_index|
            dashboard_index.go_to_new_shirt
          end

          Page::Shirt::New.perform do |shirt_new|
            shirt_new.set_name(name)
            shirt_new.create_shirt!
          end
        end
      end
    end
  end
end
```

### Define API implementation

A factory may also implement the three following methods to be able to create a
resource via the public GitLab API:

- `#api_get_path`: The `GET` path to fetch an existing resource.
- `#api_post_path`: The `POST` path to create a new resource.
- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.

Let's take the `Shirt` factory example, and add these three API methods:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        attr_accessor :name, :size

        def initialize(name)
          @name = name
        end

        def fabricate!
          Page::Dashboard::Index.perform do |dashboard_index|
            dashboard_index.go_to_new_shirt
          end

          Page::Shirt::New.perform do |shirt_new|
            shirt_new.set_name(name)
            shirt_new.create_shirt!
          end
        end

        def api_get_path
          "/shirt/#{name}"
        end

        def api_post_path
          "/shirts"
        end

        def api_post_body
          {
            name: name
          }
        end
      end
    end
  end
end
```

The [`Project` factory](./resource/project.rb) is a good real example of Browser
UI and API implementations.

### Define dependencies

A resource may need an other resource to exist first. For instance, a project
needs a group to be created in.

To define a dependency, you can use the `dependency` DSL method.
The first argument is a factory class, then you should pass `as: <name>` to give
a name to the dependency.
That will allow access to the dependency from your resource object's methods.
You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`,
`#api_post_body`.

Let's take the `Shirt` factory, and add a `project` dependency to it:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        attr_accessor :name, :size

        dependency Factory::Resource::Project, as: :project do |project|
          project.name = 'project-to-create-a-shirt'
        end

        def initialize(name)
          @name = name
        end

        def fabricate!
          project.visit!

          Page::Project::Show.perform do |project_show|
            project_show.go_to_new_shirt
          end

          Page::Shirt::New.perform do |shirt_new|
            shirt_new.set_name(name)
            shirt_new.create_shirt!
          end
        end

        def api_get_path
          "/project/#{project.path}/shirt/#{name}"
        end

        def api_post_path
          "/project/#{project.path}/shirts"
        end

        def api_post_body
          {
            name: name
          }
        end
      end
    end
  end
end
```

**Note that dependencies are always built via the API fabrication method if
supported by their factories.**

### Define attributes on the created resource

Once created, you may want to populate a resource with attributes that can be
found in the Web page, or in the API response.
For instance, once you create a project, you may want to store its repository
SSH URL as an attribute.

To define an attribute, you can use the `product` DSL method.
The first argument is the attribute name, then you should define a name for the
dependency to be accessible from your resource object's methods.

Let's take the `Shirt` factory, and define a `:brand` attribute:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        attr_accessor :name, :size

        dependency Factory::Resource::Project, as: :project do |project|
          project.name = 'project-to-create-a-shirt'
        end

        # Attribute populated from the Browser UI (using the block)
        product :brand do
          Page::Shirt::Show.perform do |shirt_show|
            shirt_show.fetch_brand_from_page
          end
        end

        def initialize(name)
          @name = name
        end

        def fabricate!
          project.visit!

          Page::Project::Show.perform do |project_show|
            project_show.go_to_new_shirt
          end

          Page::Shirt::New.perform do |shirt_new|
            shirt_new.set_name(name)
            shirt_new.create_shirt!
          end
        end

        def api_get_path
          "/project/#{project.path}/shirt/#{name}"
        end

        def api_post_path
          "/project/#{project.path}/shirts"
        end

        def api_post_body
          {
            name: name
          }
        end
      end
    end
  end
end
```

#### Inherit a factory's attribute

Sometimes, you want a resource to inherit its factory attributes. For instance,
it could be useful to pass the `size` attribute from the `Shirt` factory to the
created resource.
You can do that by defining `product :attribute_name` without a block.

Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        attr_accessor :name, :size

        dependency Factory::Resource::Project, as: :project do |project|
          project.name = 'project-to-create-a-shirt'
        end

        # Attribute inherited from the Shirt factory if present,
        # or from the Browser UI otherwise (using the block)
        product :brand do
          Page::Shirt::Show.perform do |shirt_show|
            shirt_show.fetch_brand_from_page
          end
        end

        # Attribute inherited from the Shirt factory if present,
        # or a QA::Factory::Product::NoValueError is raised otherwise
        product :name
        product :size

        def initialize(name)
          @name = name
        end

        def fabricate!
          project.visit!

          Page::Project::Show.perform do |project_show|
            project_show.go_to_new_shirt
          end

          Page::Shirt::New.perform do |shirt_new|
            shirt_new.set_name(name)
            shirt_new.create_shirt!
          end
        end

        def api_get_path
          "/project/#{project.path}/shirt/#{name}"
        end

        def api_post_path
          "/project/#{project.path}/shirts"
        end

        def api_post_body
          {
            name: name
          }
        end
      end
    end
  end
end
```

#### Define an attribute based on an API response

Sometimes, you want to define a resource attribute based on the API response
from its `GET` or `POST` request. For instance, if the creation of a shirt via
the API returns

```ruby
{
  brand: 'a-brand-new-brand',
  size: 'extra-small',
  style: 't-shirt',
  materials: [[:cotton, 80], [:polyamide, 20]]
}
```

you may want to store `style` as-is in the resource, and fetch the first value
of the first `materials` item in a `main_fabric` attribute.

For both attributes, you will need to define an inherited attribute, as shown
in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you
will need to implement the
`#transform_api_resource` method to first populate the `:main_fabric` key in the
API response so that it can be used later to automatically populate the
attribute on your resource.

If an attribute can only be retrieved from the API response, you should define
a block to give it a default value, otherwise you could get a
`QA::Factory::Product::NoValueError` when creating your resource via the
Browser UI.

Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
attributes:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        attr_accessor :name, :size

        dependency Factory::Resource::Project, as: :project do |project|
          project.name = 'project-to-create-a-shirt'
        end

        # Attribute fetched from the API response if present if present,
        # or from the Shirt factory if present,
        # or from the Browser UI otherwise (using the block)
        product :brand do
          Page::Shirt::Show.perform do |shirt_show|
            shirt_show.fetch_brand_from_page
          end
        end

        # Attribute fetched from the API response if present if present,
        # or from the Shirt factory if present,
        # or a QA::Factory::Product::NoValueError is raised otherwise
        product :name
        product :size
        product :style do
          'unknown'
        end
        product :main_fabric do
          'unknown'
        end

        def initialize(name)
          @name = name
        end

        def fabricate!
          project.visit!

          Page::Project::Show.perform do |project_show|
            project_show.go_to_new_shirt
          end

          Page::Shirt::New.perform do |shirt_new|
            shirt_new.set_name(name)
            shirt_new.create_shirt!
          end
        end

        def api_get_path
          "/project/#{project.path}/shirt/#{name}"
        end

        def api_post_path
          "/project/#{project.path}/shirts"
        end

        def api_post_body
          {
            name: name
          }
        end

        private

        def transform_api_resource(api_response)
          api_response[:main_fabric] = api_response[:materials][0][0]
          api_response
        end
      end
    end
  end
end
```

**Notes on attributes precedence:**

- attributes from the API response take precedence over attributes from the
  factory (i.e inherited)
- attributes from the factory (i.e inherited) take precedence over attributes
  from the Browser UI
- attributes without a value will raise a `QA::Factory::Product::NoValueError` error

## Creating resources in your tests

To create a resource in your tests, you can call the `.fabricate!` method on the
factory class.
Note that if the factory supports API fabrication, this will use this
fabrication by default.

Here is an example that will use the API fabrication method under the hood since
it's supported by the `Shirt` factory:

```ruby
my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt|
  shirt.size = 'small'
end

expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response
expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response
```

If you explicitely want to use the Browser UI fabrication method, you can call
the `.fabricate_via_browser_ui!` method instead:

```ruby
my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt|
  shirt.size = 'small'
end

expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute
expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block
expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block
```

You can also explicitely use the API fabrication method, by calling the
`.fabricate_via_api!` method:

```ruby
my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt|
  shirt.size = 'small'
end
```

In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`.

## Where to ask for help?

If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only).

If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab CE issue tracker with the `~QA` label.