Commit 4a0b89ae authored by Mark Lapierre's avatar Mark Lapierre

Merge branch 'ml-share-qa-test-setup' into 'master'

Allow test resources to be reused

See merge request gitlab-org/gitlab!74570
parents ada79973 c976ece1
......@@ -4,14 +4,21 @@ group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Resource class in GitLab QA
# Resource classes in GitLab QA
Resources are primarily created using Browser UI steps, but can also
be created via the API or the CLI.
Resources are primarily created using Browser UI steps, but can also be created via the API or the CLI.
A typical resource class is used to create a new resource that can be used in a single test. However, several tests can
end up creating the same kind of resource and use it in ways that mean it could have been
used by more than one test. Creating a new resource each time is not efficient. Therefore, we can also create reusable
resources that are created once and can then be used by many tests.
In the following section the content focuses on single-use resources, however it also applies to reusable resources.
Information specific to [reusable resources is detailed below](#reusable-resources).
## How to properly implement a resource class?
All resource classes should inherit from `Resource::Base`.
All non-reusable resource classes should inherit from `Resource::Base`.
There is only one mandatory method to implement to define a resource class.
This is the `#fabricate!` method, which is used to build the resource via the
......@@ -391,6 +398,96 @@ end
In this case, the result is similar to calling `Resource::Shirt.fabricate!`.
## Reusable resources
Reusable resources are created by the first test that needs a particular kind of resource, and then any test that needs
the same kind of resource can reuse it instead of creating a new one.
The `ReusableProject` resource is an example of this class:
```ruby
module QA
module Resource
class ReusableProject < Project # A reusable resource inherits from the resource class that we want to be able to reuse.
prepend Reusable # The Reusable module mixes in some methods that help implement reuse.
def initialize
super # A ReusableProject is a Project so it should be initialized as one.
# Some Project attributes aren't valid and need to be overridden. For example, a ReusableProject keeps its name once it's created,
# so we don't add a random string to the name specified.
@add_name_uuid = false
# It has a default name, and a different name can be specified when a resource is first created. However, the same name must be
# provided any time that instance of the resource is used.
@name = "reusable_project"
# Several instances of a ReusableProject can exists as long as each is identified via a unique value for `reuse_as`.
@reuse_as = :default_project
end
# All reusable resource classes must validate that an instance meets the conditions that allow reuse. For example,
# by confirming that the name specified for the instance is valid and doesn't conflict with other instances.
def validate_reuse_preconditions
raise ResourceReuseError unless reused_name_valid?
end
end
end
end
```
Consider some examples of how a reusable resource is used:
```ruby
# This will create a project.
default_project = Resource::ReusableProject.fabricate_via_api!
default_project.name # => "reusable_project"
default_project.reuse_as # => :default_project
```
Then in another test we could reuse the project:
```ruby
# This will fetch the project created above rather than creating a new one.
default_project_again = Resource::ReusableProject.fabricate_via_api!
default_project_again.name # => "reusable_project"
default_project_again.reuse_as # => :default_project
```
We can also create another project that we want to change in a way that might not be suitable for tests using the
default project:
```ruby
project_with_member = Resource::ReusableProject.fabricate_via_api! do |project|
project.name = "project-with-member"
project.reuse_as = :project_with_member
end
project_with_member.add_member(user)
```
Another test can reuse that project:
```ruby
project_still_has_member = Resource::ReusableProject.fabricate_via_api! do |project|
project.name = "project-with-member"
project.reuse_as = :project_with_member
end
expect(project_still_has_member).to have_member(user)
```
However, if we don't provide the name again an error will be raised:
```ruby
Resource::ReusableProject.fabricate_via_api! do |project|
project.reuse_as = :project_with_member
end
# => ResourceReuseError will be raised because it will try to use the default name, "reusable_project", which doesn't
# match the name specified when the project was first fabricated.
```
## Where to ask for help?
If you need more information, ask for help on `#quality` channel on Slack
......
......@@ -7,14 +7,7 @@ module QA
module Resource
module ApiFabricator
include Capybara::DSL
ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceNotDeletedError = Class.new(RuntimeError)
ResourceNotFoundError = Class.new(RuntimeError)
ResourceQueryError = Class.new(RuntimeError)
ResourceUpdateFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
InternalServerError = Class.new(RuntimeError)
include Errors
attr_reader :api_resource, :api_response
attr_writer :api_client
......
# frozen_string_literal: true
module QA
module Resource
module Errors
ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceNotDeletedError = Class.new(RuntimeError)
ResourceNotFoundError = Class.new(RuntimeError)
ResourceQueryError = Class.new(RuntimeError)
ResourceUpdateFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
InternalServerError = Class.new(RuntimeError)
end
end
end
......@@ -7,7 +7,8 @@ module QA
:author_name,
:content,
:commit_message,
:name
:name,
:start_branch
attr_writer :branch
attribute :project do
......@@ -27,6 +28,7 @@ module QA
@name = 'QA Test - File name'
@content = 'QA Test - File content'
@commit_message = 'QA Test - Commit message'
@start_branch = project.default_branch
end
def branch
......@@ -57,6 +59,7 @@ module QA
def api_post_body
{
branch: branch,
start_branch: start_branch,
author_email: @author_email || Runtime::User.default_email,
author_name: @author_name || Runtime::User.username,
content: content,
......
# frozen_string_literal: true
module QA
module Resource
#
# This module includes methods that allow resource classes to be reused safely. It should be prepended to a new
# reusable version of an existing resource class. See Resource::Project and ReusableResource::Project for an example
#
module Reusable
attr_accessor :reuse,
:reuse_as
ResourceReuseError = Class.new(RuntimeError)
def self.prepended(base)
base.extend(ClassMethods)
end
# Gets an existing resource if it exists and the parameters of the new specification of the resource are valid.
# Creates a new instance of the resource if it does not exist.
#
# @return [String] The URL of the resource.
def fabricate_via_api!
validate_reuse_preconditions
resource_web_url(api_get)
rescue Errors::ResourceNotFoundError
super
ensure
self.class.resources[reuse_as] = self
end
# Including classes must confirm that the resource can be reused as defined. For example, a project can't be
# fabricated with a unique name.
#
# @return [nil]
def validate_reuse_preconditions
return super if defined?(super)
raise NotImplementedError
end
module ClassMethods
# Removes all created resources of this type.
#
# @return [Hash<Symbol, QA::Resource>] the resources that were to be removed.
def remove_all_via_api!
resources.each do |reuse_as, resource|
QA::Runtime::Logger.debug("#{self.name} - removing #{reuse_as}")
resource.method(:remove_via_api!).super_method.call
end
end
# The resources created by this resource class.
#
# @return [Hash<Symbol, QA::Resource>] the resources created by this resource class.
def resources
@resources ||= {}
end
end
end
end
end
# frozen_string_literal: true
module QA
module Resource
class ReusableProject < Project
prepend Reusable
def initialize
super
@add_name_uuid = false
@name = "reusable_project"
@reuse_as = :default_project
@initialize_with_readme = true
end
# Confirms that the project can be reused
#
# @return [nil] returns nil unless an error is raised
def validate_reuse_preconditions
unless reused_name_unique?
raise ResourceReuseError,
"Reusable projects must have the same name. The project reused as #{reuse_as} has the name '#{name}' but it should be '#{self.class.resources[reuse_as].name}'"
end
end
# Checks if the project is being reused with the same name.
#
# @return [Boolean] true if the project's name is different from another project with the same reuse symbol (reuse_as)
def reused_name_unique?
return true unless self.class.resources.key?(reuse_as)
self.class.resources[reuse_as].name == name
end
# Overrides QA::Resource::Project#remove_via_api! to log a debug message stating that removal will happen after
# the suite completes rather than now.
#
# @return [nil]
def remove_via_api!
QA::Runtime::Logger.debug("#{self.class.name} - deferring removal until after suite")
end
end
end
end
......@@ -4,7 +4,10 @@ module QA
RSpec.describe 'Manage' do
describe 'Project access token' do
before(:all) do
@project_access_token = QA::Resource::ProjectAccessToken.fabricate_via_api!
@project_access_token = QA::Resource::ProjectAccessToken.fabricate_via_api! do |pat|
pat.project = Resource::ReusableProject.fabricate_via_api!
end
@user_api_client = Runtime::API::Client.new(:gitlab, personal_access_token: @project_access_token.token)
end
......@@ -14,7 +17,7 @@ module QA
Resource::File.fabricate_via_api! do |file|
file.api_client = @user_api_client
file.project = @project_access_token.project
file.branch = 'new_branch'
file.branch = "new_branch_#{SecureRandom.hex(8)}"
file.commit_message = 'Add new file'
file.name = "text-#{SecureRandom.hex(8)}.txt"
file.content = 'New file'
......@@ -27,7 +30,7 @@ module QA
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.api_client = @user_api_client
commit.project = @project_access_token.project
commit.branch = 'new_branch'
commit.branch = "new_branch_#{SecureRandom.hex(8)}"
commit.start_branch = @project_access_token.project.default_branch
commit.commit_message = 'Add new file'
commit.add_files([
......@@ -48,7 +51,7 @@ module QA
Resource::File.fabricate_via_api! do |file|
file.api_client = @user_api_client
file.project = @different_project
file.branch = 'new_branch'
file.branch = "new_branch_#{SecureRandom.hex(8)}"
file.commit_message = 'Add new file'
file.name = "text-#{SecureRandom.hex(8)}.txt"
file.content = 'New file'
......@@ -61,7 +64,7 @@ module QA
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.api_client = @user_api_client
commit.project = @different_project
commit.branch = 'new_branch'
commit.branch = "new_branch_#{SecureRandom.hex(8)}"
commit.start_branch = @different_project.default_branch
commit.commit_message = 'Add new file'
commit.add_files([
......
......@@ -11,12 +11,7 @@ module QA
let(:title) { "MR push options test #{SecureRandom.hex(8)}" }
let(:commit_message) { 'Add README.md' }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'merge-request-push-options'
project.initialize_with_readme = true
end
end
let(:project) { Resource::ReusableProject.fabricate_via_api! }
def create_new_mr_via_push
Resource::Repository::ProjectPush.fabricate! do |push|
......
......@@ -64,6 +64,13 @@ RSpec.configure do |config|
end
end
config.after(:suite) do |suite|
# If any tests failed, leave the resources behind to help troubleshoot
next if suite.reporter.failed_examples.present?
QA::Resource::ReusableProject.remove_all_via_api!
end
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment