GitLab QA のリソースクラス
リソースは主にブラウザUIのステップを使って作成しますが、APIやCLIを使って作成することもできます。
リソースクラスを適切に実装するには?
すべてのリソースクラスはResource::Base
を継承しなければなりません。
リソースクラスを定義するために実装が必須なメソッドは1つだけです。これは#fabricate!
メソッドで、ブラウザの UI を通じてリソースを構築するために使用されます。このメソッドでは、Pages オブジェクトのみを使用して Web ページと対話する必要があることに注意してください。
以下は想像上の例です:
module QA
module Resource
class Shirt < 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
API実装の定義
リソースクラスは、公開GitLab API経由でリソースを作成できるように、以下の3つのメソッドを実装することもできます:
-
#api_get_path
:既存のリソースを取得するためのGET
パス。 -
#api_post_path
:新しいリソースを作成するためのPOST
パス。 -
#api_post_body
:新しいリソースを作成するPOST
本体 (Ruby ハッシュ)。
多くのAPIリソースはページ分割されていることに注意してください。期待する結果が見つからない場合は、結果が複数ページにわたっているかどうかを確認してください。
Shirt
リソース・クラスに、これら3つのAPIメソッドを追加してみましょう:
module QA
module Resource
class Shirt < 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
Project
リソースは、ブラウザ UI と API 実装の良い実例です。
リソースの属性
リソースは、最初に他のリソースが存在する必要があります。例えば、プロジェクトにはグループが必要です。
リソース属性を定義するには、attribute
メソッドを、他のリソースクラスを使用するブロックと一緒に使用して、リソースを作成します。
これにより、リソースオブジェクトのメソッドから他のリソースにアクセスできるようになります。通常は#fabricate!
,#api_get_path
,#api_post_path
,#api_post_body
で使用します。
Shirt
リソース・クラスにproject
属性を追加してみましょう:
module QA
module Resource
class Shirt < Base
attr_accessor :name
attribute :project do
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
すべての属性は遅延的に構築されることに注意してください。つまり、特定の属性を最初に作成したい場合は、その属性を使用していなくても、最初に属性メソッドを呼び出す必要があります。
製品データの属性
一度作成したリソースに、Web ページや API レスポンスに含まれる属性を設定したい場合があります。例えば、プロジェクトを作成したら、そのリポジトリの SSH URL を属性として保存したいと思うかもしれません。
この場合も、attribute
メソッドをブロックで使用し、ページオブジェクトを使用してページ上のデータを取得することができます。
Shirt
リソース・クラスで、:brand
属性を定義してみましょう:
module QA
module Resource
class Shirt < Base
attr_accessor :name
attribute :project do
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
すべての属性が遅延的に構築されていることに注意してください。つまり、他のページに移動した後にshirt.brand
を呼び出すと、期待したページではなくなってしまうため、適切にデータを取得できないということです。
考えてみてください:
shirt =
QA::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.project.visit!
shirt.brand # => FAIL!
上記の例は、今プロジェクトページにいて、シャツページからブランドデータを構築しようとしているため、失敗します。これを解決する方法は2つあります。1つは、プロジェクトに再度アクセスする前にブランドを取得することです:
shirt =
QA::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.brand # => OK!
shirt.project.visit!
shirt.brand # => OK!
属性はインスタンスに保存されているため、次の呼び出しはすべて、以前に構築されたデータを使用して問題ありません。もし、これではもろいと思うのであれば、ファブリケーションを終了する直前にデータを作成することもできます:
module QA
module Resource
class Shirt < 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
populate(:brand) # Eagerly construct the data
end
end
end
end
populate
メソッドは引数を繰り返し、各属性を呼び出します。ここでpopulate(:brand)
はbrand
と同じ効果があります。populateメソッドを使うことで、意図がより明確になります。
これによって、シャツを作成した直後にデータを構築することが保証されます。欠点は、データを使用する必要がない場合でも、リソースを作成するときに常にデータを作成することです。
あるいは、ブランドデータを構築する前に、正しいページにいることを確認することもできます:
module QA
module Resource
class Shirt < Base
attr_accessor :name
attribute :project do
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
これは、ブランドを構築する前にシャツのページであることを確認し、状態を壊さないように前のページに戻ります。
API レスポンスに基づく属性の定義
GET
またはPOST
リクエストからの API レスポンスに基づいてリソース属性を定義したい場合があります。例えば、API 経由でシャツを作成すると
{
brand: 'a-brand-new-brand',
style: 't-shirt',
materials: [[:cotton, 80], [:polyamide, 20]]
}
を返した場合、style
をそのままリソースに格納し、main_fabric
属性で最初のmaterials
項目の最初の値を取得したい場合があります。
Shirt
リソース・クラスで、:style
と:main_fabric
属性を定義してみましょう:
module QA
module Resource
class Shirt < Base
# ... same as before
# @style from the instance if present,
# or fetched from the API response if present,
# or a QA::Resource::Base::NoValueError is raised otherwise
attribute :style
# If @main_fabric 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, and
# store the result in @main_fabric
attribute :main_fabric do
api_response.&dig(:materials, 0, 0)
end
# ... same as before
end
end
end
属性の優先順位に関する注意
- リソースインスタンス変数が最も優先されます。
- APIレスポンスからの属性は、ブロックからの属性(通常はブラウザUIからの属性)よりも優先されます。
- 値のない属性は、
QA::Resource::Base::NoValueError
エラーが発生します。
テストでのリソースの作成
テストの中でリソースを作成するには、リソースクラスの.fabricate!
メソッドをコールするか、 あるいはファクトリを使用します。リソースクラスが API ファブリケーションをサポートしている場合は、 デフォルトでこのファブリケーションを使用することに注意しましょう。
ここでは、Shirt
リソースクラスでサポートされている API ファブリケーションメソッドを内部で使用する例を示します:
my_shirt = Resource::Shirt.fabricate! do |shirt|
shirt.name = 'my-shirt'
end
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable
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
明示的にブラウザUIのファブリケーションメソッドを使用したい場合は、代わりに.fabricate_via_browser_ui!
:
my_shirt = 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 resource's instance variable
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::Resource::Base::NoValueError will be raised because no API response nor a block is provided
expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::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)
また、.fabricate_via_api!
メソッドを呼び出すことで、API製作メソッドを明示的に使用することもできます:
my_shirt = Resource::Shirt.fabricate_via_api! do |shirt|
shirt.name = 'my-shirt'
end
この場合、結果はResource::Shirt.fabricate!
を呼び出すのと同様です。
ファクトリー
FactoryBotを使用してリソースを作成することもできます。
# create a project via the API to use in the test
let(:project) { create(:project) }
# create an issue belonging to a project via the API to use in the test
let(:issue) { create(:issue, project: project) }
# create a private project via the API with a specific name
let(:project) { create(:project, :private, name: 'my-project-name', add_name_uuid: false) }
すべてのファクトリはqa/qa/factories
で定義されており、それぞれのQA::Resource::Base
クラスを代表するものです。
例えば、ファクトリー:issue
はqa/resource/issue.rb
にあります。ファクトリー:project
はqa/resource/project.rb
にあります。
新しいファクトリーの作成
リソースが与えられると
# qa/resource/shirt.rb
module QA
module Resource
class Shirt < Base
attr_accessor :name
attr_reader :read_only
attribute :brand
def api_post_body
{ name: name, brand: brand }
end
end
end
end
デフォルトとオーバーライドでファクトリーを定義します:
# qa/factories/shirts.rb
module QA
FactoryBot.define do
factory :shirt, class: 'QA::Resource::Shirt' do
brand { 'BrandName' }
trait :with_name do
name { 'Shirt Name' }
end
end
end
end
テストでは、API 経由でリソースを作成します:
let(:my_shirt) { create(:shirt, brand: 'AnotherBrand') } #<Resource::Shirt @brand="AnotherBrand" @name=nil>
let(:named_shirt) { create(:shirt, :with_name) } #<Resource::Shirt @brand="Brand Name" @name="Shirt Name">
let(:invalid_shirt) { create(:shirt, read_only: true) } # NoMethodError
it 'creates a shirt' do
expect(my_shirt.brand).to eq('AnotherBrand')
expect(named_shirt.name).to eq('Shirt Name')
expect(invalid_shirt).to raise_error(NoMethodError) # tries to call Resource::Shirt#read_only=
end
リソースのクリーンアップ
テスト実行中に作成されたすべてのリソースを収集する仕組みと、これらのリソースを処理する仕組みがあります。ドットコム環境では、テストスイートがQA パイプラインで終了すると、すべての合格テストのリソースは同じパイプラインの実行で自動的に削除されます。すべての不合格テストのリソースは調査用に予約され、翌週の土曜日にスケジュールされたパイプラインが実行されるまで削除されません。新しいリソースを導入する場合は、削除できないリソースもIGNORED_RESOURCESリストに追加してください。
どこで助けを求めますか?
より詳しい情報が必要な場合は、Slack の#quality
チャンネルで助けを求めてください(内部、GitLab チームのみ)。
チームメンバーでなく、貢献するために助けが必要な場合は、GitLab CE issue tracker で~QA
ラベルを付けてイシューを開いてください。