summaryrefslogtreecommitdiff
path: root/qa/qa/factory/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'qa/qa/factory/README.md')
-rw-r--r--qa/qa/factory/README.md445
1 files changed, 445 insertions, 0 deletions
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
new file mode 100644
index 00000000000..cfce096ab39
--- /dev/null
+++ b/qa/qa/factory/README.md
@@ -0,0 +1,445 @@
+# 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
+
+ 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
+
+ def fabricate!
+ # ... same as before
+ 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 attributes
+
+After the resource is fabricated, we would like to access the attributes on
+the resource. We define the attributes with `attribute` method. Suppose
+we want to access the name on the resource, we could change `attr_accessor`
+to `attribute`:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attribute :name
+
+ # ... same as before
+ end
+ end
+ end
+end
+```
+
+The difference between `attr_accessor` and `attribute` is that by using
+`attribute` it can also be accessed from the product:
+
+```ruby
+shirt =
+ QA::Factory::Resource::Shirt.fabricate! do |resource|
+ resource.name = "GitLab QA"
+ end
+
+shirt.name # => "GitLab QA"
+```
+
+In the above example, if we use `attr_accessor :name` then `shirt.name` won't
+be available. On the other hand, using `attribute :name` will allow you to use
+`shirt.name`, so most of the time you'll want to use `attribute` instead of
+`attr_accessor` unless we clearly don't need it for the product.
+
+#### Resource attributes
+
+A resource may need another resource to exist first. For instance, a project
+needs a group to be created in.
+
+To define a resource attribute, you can use the `attribute` method with a
+block using the other factory to fabricate the resource.
+
+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:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attribute :name
+
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ 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 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.**
+
+#### Product data attributes
+
+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.
+
+Again we could use the `attribute` method with a block, using a page object
+to retrieve the data on the page.
+
+Let's take the `Shirt` factory, and define a `:brand` attribute:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attribute :name
+
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ # ... 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
+
+ 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
+
+ brand # Eagerly construct the data
+ end
+ end
+ end
+ end
+end
+```
+
+This will make sure we construct the data right after we created the shirt.
+The drawback for this will become we're forced to construct the data even
+if we don't really need to use it.
+
+Alternatively, we could just make sure we're on the right page before
+constructing the brand data:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attribute :name
+
+ attribute :project do
+ Factory::Resource::Project.fabricate! do |resource|
+ resource.name = 'project-to-create-a-shirt'
+ end
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ attribute :brand do
+ back_url = current_url
+ visit!
+
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+
+ visit(back_url)
+ end
+
+ # ... same as before
+ end
+ end
+ end
+end
+```
+
+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.
+
+#### 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
+ # ... same as before
+
+ # 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
+
+ # 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)
+ end
+
+ # ... same as before
+ end
+ end
+ end
+end
+```
+
+**Notes on attributes precedence:**
+
+- attributes from the factory have the highest precedence
+- attributes from the API response take precedence over attributes from the
+ block (usually from Browser UI)
+- attributes without a value will raise a `QA::Factory::Base::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! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
+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
+expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
+```
+
+If you explicitly 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! do |shirt|
+ shirt.name = 'my-shirt'
+end
+
+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)
+```
+
+You can also explicitly use the API fabrication method, by calling the
+`.fabricate_via_api!` method:
+
+```ruby
+my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
+ shirt.name = 'my-shirt'
+end
+```
+
+In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
+
+## 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.