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 クラスを代表するものです。

例えば、ファクトリー:issueqa/resource/issue.rbにあります。ファクトリー:projectqa/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 ラベルを付けてイシューを開いてください。