README.md 11.1 KB
Newer Older
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
# 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
29
        attr_accessor :name
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

        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
63
        attr_accessor :name
64 65

        def fabricate!
66
          # ... same as before
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
        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.

91 92 93
#### Resource attributes

A resource may need another resource to exist first. For instance, a project
94 95
needs a group to be created in.

96 97
To define a resource attribute, you can use the `attribute` method with a
block using the other factory to fabricate the resource.
98

99 100 101 102 103
That will allow access to the other resource 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` attribute to it:
104 105 106 107 108 109

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
110
        attr_accessor :name
111

112 113 114 115
        attribute :project do
          Factory::Resource::Project.fabricate! do |resource|
            resource.name = 'project-to-create-a-shirt'
          end
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
        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
```

150 151 152
**Note that all the attributes are lazily constructed. This means if you want
a specific attribute to be fabricated first, you'll need to call the
attribute method first even if you're not using it.**
153

154
#### Product data attributes
155 156 157 158 159 160

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.

161 162
Again we could use the `attribute` method with a block, using a page object
to retrieve the data on the page.
163 164 165 166 167 168 169 170

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

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
171
        attr_accessor :name
172

173 174 175 176
        attribute :project do
          Factory::Resource::Project.fabricate! do |resource|
            resource.name = 'project-to-create-a-shirt'
          end
177 178 179
        end

        # Attribute populated from the Browser UI (using the block)
180
        attribute :brand do
181 182 183 184 185
          Page::Shirt::Show.perform do |shirt_show|
            shirt_show.fetch_brand_from_page
          end
        end

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
        # ... same as before
      end
    end
  end
end
```

**Note again that all the attributes are lazily constructed. This means if
you call `shirt.brand` after moving to the other page, it'll not properly
retrieve the data because we're no longer on the expected page.**

Consider this:

```ruby
shirt =
  QA::Factory::Resource::Shirt.fabricate! do |resource|
    resource.name = "GitLab QA"
  end

shirt.project.visit!

shirt.brand # => FAIL!
```

The above example will fail because now we're on the project page, trying to
construct the brand data from the shirt page, however we moved to the project
page already. There are two ways to solve this, one is that we could try to
retrieve the brand before visiting the project again:

```ruby
shirt =
  QA::Factory::Resource::Shirt.fabricate! do |resource|
    resource.name = "GitLab QA"
  end

shirt.brand # => OK!

shirt.project.visit!

shirt.brand # => OK!
```

The attribute will be stored in the instance therefore all the following calls
will be fine, using the data previously constructed. If we think that this
might be too brittle, we could eagerly construct the data right before
ending fabrication:

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
        # ... same as before
239 240 241 242 243 244 245 246 247 248 249 250 251

        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

252
          populate(:brand) # Eagerly construct the data
253 254 255 256 257 258 259
        end
      end
    end
  end
end
```

260 261 262 263 264 265
The `populate` method will iterate through its arguments and call each
attribute respectively. Here `populate(:brand)` has the same effect as
just `brand`. Using the populate method makes the intention clearer.

With this, it will make sure we construct the data right after we create the
shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data.
266

267 268
Alternatively, we could just make sure we're on the right page before
constructing the brand data:
269 270 271 272 273 274

```ruby
module QA
  module Factory
    module Resource
      class Shirt < Factory::Base
275
        attr_accessor :name
276

277 278 279
        attribute :project do
          Factory::Resource::Project.fabricate! do |resource|
            resource.name = 'project-to-create-a-shirt'
280 281 282
          end
        end

283 284 285 286
        # Attribute populated from the Browser UI (using the block)
        attribute :brand do
          back_url = current_url
          visit!
287

288 289
          Page::Shirt::Show.perform do |shirt_show|
            shirt_show.fetch_brand_from_page
290 291
          end

292
          visit(back_url)
293 294
        end

295
        # ... same as before
296 297 298 299 300 301
      end
    end
  end
end
```

302 303 304
This will make sure it's on the shirt page before constructing brand, and
move back to the previous page to avoid breaking the state.

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
#### 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',
  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.

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
330
        # ... same as before
331

332 333 334 335
        # Attribute from the Shirt factory if present,
        # or fetched from the API response if present,
        # or a QA::Factory::Base::NoValueError is raised otherwise
        attribute :style
336

337 338 339 340 341
        # If the attribute from the Shirt factory is not present,
        # and if the API does not contain this field, this block will be
        # used to construct the value based on the API response.
        attribute :main_fabric do
          api_response.&dig(:materials, 0, 0)
342 343
        end

344
        # ... same as before
345 346 347 348 349 350 351 352
      end
    end
  end
end
```

**Notes on attributes precedence:**

353
- factory instance variables have the highest precedence
354
- attributes from the API response take precedence over attributes from the
355 356
  block (usually from Browser UI)
- attributes without a value will raise a `QA::Factory::Base::NoValueError` error
357 358 359 360 361 362 363 364 365 366 367 368

## 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
369 370
my_shirt = Factory::Resource::Shirt.fabricate! do |shirt|
  shirt.name = 'my-shirt'
371 372
end

373
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
374 375
expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
376
expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
377 378
```

379
If you explicitly want to use the Browser UI fabrication method, you can call
380 381 382
the `.fabricate_via_browser_ui!` method instead:

```ruby
383 384
my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt|
  shirt.name = 'my-shirt'
385 386
end

387 388 389 390
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided
expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
391 392
```

393
You can also explicitly use the API fabrication method, by calling the
394 395 396
`.fabricate_via_api!` method:

```ruby
397 398
my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
  shirt.name = 'my-shirt'
399 400 401
end
```

402
In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
403 404 405 406 407 408 409 410

## 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.