GitLab QAのリソースクラス

リソースは主にブラウザUIのステップを使用して作成されますが、APIやCLIを使用して作成することもできます。

リソースクラスを適切に実装するには?

すべてのリソースクラスはResource::Baseを継承する必要があります。

リソースクラスを定義するために実装が必須なメソッドは1つだけです。 これは#fabricate! メソッドで、ブラウザのUIを介してリソースを構築するために使用されます。 このメソッドでは、Webページと対話するためにPageオブジェクトのみを使用する必要があることに注意してください。

これは想像上の例です:

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!

この属性はインスタンスに保存されるため、次の呼び出しはすべて、以前に構築されたデータを使用して問題ありません。 これがあまりにもろいと思われる場合は、ファブリケーションを終了する直前にデータをeagerlyに構築することもできます:

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!を呼び出すのと同じようになります。

どこに助けを求めますか?

より詳しい情報が必要な場合は、Slack の#quality チャンネルで助けを求めてください(内部、GitLab チームのみ)。

チームメンバーでなくても貢献したい場合は、GitLab CE issue trackerに~QA のラベルを付けてイシューを登録してください。