InfoGrab DocsInfoGrab Docs

GitLab QA의 리소스 클래스

요약

리소스는 주로 브라우저 UI 단계를 사용하여 생성되지만, API 또는 CLI를 통해 생성할 수도 있습니다. 모든 리소스 클래스는 Resource::Base를 상속해야 합니다. 리소스 클래스를 정의하기 위해 반드시 구현해야 하는 메서드는 단 하나입니다.

리소스는 주로 브라우저 UI 단계를 사용하여 생성되지만, API 또는 CLI를 통해 생성할 수도 있습니다.

리소스 클래스를 올바르게 구현하는 방법#

모든 리소스 클래스는 Resource::Base를 상속해야 합니다.

리소스 클래스를 정의하기 위해 반드시 구현해야 하는 메서드는 단 하나입니다. 바로 #fabricate! 메서드로, 브라우저 UI를 통해 리소스를 빌드하는 데 사용됩니다. 이 메서드에서 웹 페이지와 상호작용할 때는 페이지 객체만 사용해야 합니다.

다음은 가상의 예시입니다:

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를 통해 리소스를 생성할 수 있도록 다음 세 가지 메서드를 추가로 구현할 수 있습니다:

  • #api_get_path: 기존 리소스를 가져오는 GET 경로.

  • #api_post_path: 새 리소스를 생성하는 POST 경로.

  • #api_post_body: 새 리소스를 생성하는 POST 본문(Ruby 해시 형태).

많은 API 리소스는 페이지네이션을 적용한다는 점에 유의하세요. 원하는 결과를 찾지 못한다면 결과가 여러 페이지에 걸쳐 있는지 확인하세요.

Shirt 리소스 클래스에 세 가지 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)할 수 있습니다.

이를 통해 리소스 객체의 메서드에서 다른 리소스에 접근할 수 있습니다. 일반적으로 #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

모든 속성은 지연 생성(lazily constructed)된다는 점에 유의하세요. 즉, 특정 속성을 먼저 생성하려면 해당 속성을 사용하지 않더라도 attribute 메서드를 먼저 호출해야 합니다.

제품 데이터 속성#

리소스를 생성한 후, 웹 페이지나 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!

위 예시는 현재 프로젝트 페이지에 있는 상태에서 셔츠 페이지의 브랜드 데이터를 구성하려 했지만, 이미 프로젝트 페이지로 이동했기 때문에 실패합니다. 이를 해결하는 방법은 두 가지입니다. 하나는 프로젝트를 다시 방문하기 전에 브랜드를 먼저 가져오는 것입니다:

shirt =
  QA::Resource::Shirt.fabricate! do |resource|
    resource.name = "GitLab QA"
  end

shirt.brand # => OK!

shirt.project.visit!

shirt.brand # => OK!

속성은 인스턴스에 저장되므로, 이후의 모든 호출은 이전에 구성된 데이터를 사용하여 정상적으로 동작합니다. 이 방법이 너무 취약할 수 있다고 생각된다면, fabrication이 끝나기 직전에 데이터를 즉시 구성할 수 있습니다:

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 메서드를 사용하면 의도를 더 명확하게 표현할 수 있습니다.

이렇게 하면 셔츠를 생성한 직후에 데이터를 구성하는 것이 보장됩니다. 단점은 데이터를 사용할 필요가 없더라도 리소스가 fabricate될 때마다 항상 데이터를 구성한다는 것입니다.

대안으로, 브랜드 데이터를 구성하기 전에 올바른 페이지에 있는지 확인하는 방법도 있습니다:

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은 그대로 리소스에 저장하고, materials의 첫 번째 항목의 첫 번째 값을 main_fabric 속성으로 가져오고 싶을 수 있습니다.

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! 메서드를 호출하거나, factory를 사용하여 생성할 수 있습니다. 리소스 클래스가 API fabrication을 지원하는 경우, 기본적으로 API fabrication을 사용한다는 점에 유의하세요.

다음은 Shirt 리소스 클래스가 지원하기 때문에 내부적으로 API fabrication 메서드를 사용하는 예시입니다:

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 fabrication 메서드를 명시적으로 사용하려면 .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 fabrication 메서드를 명시적으로 사용할 수도 있습니다:

my_shirt = Resource::Shirt.fabricate_via_api! do |shirt|
  shirt.name = 'my-shirt'
end

이 경우, 결과는 Resource::Shirt.fabricate!를 호출하는 것과 유사합니다.

Factories#

테스트 내에서 리소스를 생성, 빌드, 가져오기 위해 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) }

# create one commit in a project that performs three actions
let(:commit) do
  create(:commit, commit_message: 'my message', project: project, actions: [
    { action: 'create', file_path: 'README.md', content: '# Welcome!' },
    { action: 'update', file_path: 'README.md', content: '# Updated' },
    { action: 'delete', file_path: 'README.md' }
  ])
end

###

# instantiate an Issue but don't create it via API yet
let(:issue) { build(:issue) }

# instantiate a Project and perform some actions before creating
let(:project) do
  build(:project) do |p|
    p.name = 'Test'
    p.add_name_uuid = false
  end
end

# fetch an existing issue via the API with attributes
let(:existing_issue) { build(:issue, project: project, iid: issue.iid).reload! }

모든 factory는 qa/qa/factories에 정의되어 있으며, 각각의 QA::Resource::Base 클래스를 대표합니다.

예를 들어, factory :issueqa/resource/issue.rb에서 찾을 수 있습니다. factory :projectqa/resource/project.rb에서 찾을 수 있습니다.

새 Factory 생성하기#

다음과 같은 리소스가 있다고 가정합니다:

# 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

기본값과 재정의가 있는 factory를 정의합니다:

# 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') } #
let(:named_shirt) { create(:shirt, :with_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

리소스 정리#

테스트 실행 중에 생성된 모든 리소스를 수집하는 메커니즘과, 이러한 리소스를 처리하는 메커니즘이 있습니다. dotcom 환경에서는 E2E 파이프라인에서 테스트 스위트가 완료된 후, 통과한 모든 테스트의 리소스가 동일한 파이프라인 실행에서 자동으로 삭제됩니다. 실패한 모든 테스트의 리소스는 조사를 위해 보존되며, 예약된 파이프라인에 의해 다음 토요일까지 삭제되지 않습니다. 새 리소스를 도입할 때는 삭제할 수 없는 리소스를 IGNORED_RESOURCES 목록에 추가해야 합니다.

도움을 받을 수 있는 곳은?#

더 많은 정보가 필요하다면 Slack의 #s_developer_experience 채널에서 도움을 요청하세요(내부용, GitLab 팀 전용).

GitLab 팀 멤버가 아니지만 기여를 위해 도움이 필요하다면, ~QA 라벨을 붙여 GitLab 이슈 트래커에 이슈를 열어 주세요.

GitLab QA의 리소스 클래스

GitLab v19.1
원문 보기
요약

리소스는 주로 브라우저 UI 단계를 사용하여 생성되지만, API 또는 CLI를 통해 생성할 수도 있습니다. 모든 리소스 클래스는 Resource::Base를 상속해야 합니다. 리소스 클래스를 정의하기 위해 반드시 구현해야 하는 메서드는 단 하나입니다.

리소스는 주로 브라우저 UI 단계를 사용하여 생성되지만, API 또는 CLI를 통해 생성할 수도 있습니다.

리소스 클래스를 올바르게 구현하는 방법#

모든 리소스 클래스는 Resource::Base를 상속해야 합니다.

리소스 클래스를 정의하기 위해 반드시 구현해야 하는 메서드는 단 하나입니다. 바로 #fabricate! 메서드로, 브라우저 UI를 통해 리소스를 빌드하는 데 사용됩니다. 이 메서드에서 웹 페이지와 상호작용할 때는 페이지 객체만 사용해야 합니다.

다음은 가상의 예시입니다:

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를 통해 리소스를 생성할 수 있도록 다음 세 가지 메서드를 추가로 구현할 수 있습니다:

  • #api_get_path: 기존 리소스를 가져오는 GET 경로.

  • #api_post_path: 새 리소스를 생성하는 POST 경로.

  • #api_post_body: 새 리소스를 생성하는 POST 본문(Ruby 해시 형태).

많은 API 리소스는 페이지네이션을 적용한다는 점에 유의하세요. 원하는 결과를 찾지 못한다면 결과가 여러 페이지에 걸쳐 있는지 확인하세요.

Shirt 리소스 클래스에 세 가지 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)할 수 있습니다.

이를 통해 리소스 객체의 메서드에서 다른 리소스에 접근할 수 있습니다. 일반적으로 #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

모든 속성은 지연 생성(lazily constructed)된다는 점에 유의하세요. 즉, 특정 속성을 먼저 생성하려면 해당 속성을 사용하지 않더라도 attribute 메서드를 먼저 호출해야 합니다.

제품 데이터 속성#

리소스를 생성한 후, 웹 페이지나 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!

위 예시는 현재 프로젝트 페이지에 있는 상태에서 셔츠 페이지의 브랜드 데이터를 구성하려 했지만, 이미 프로젝트 페이지로 이동했기 때문에 실패합니다. 이를 해결하는 방법은 두 가지입니다. 하나는 프로젝트를 다시 방문하기 전에 브랜드를 먼저 가져오는 것입니다:

shirt =
  QA::Resource::Shirt.fabricate! do |resource|
    resource.name = "GitLab QA"
  end

shirt.brand # => OK!

shirt.project.visit!

shirt.brand # => OK!

속성은 인스턴스에 저장되므로, 이후의 모든 호출은 이전에 구성된 데이터를 사용하여 정상적으로 동작합니다. 이 방법이 너무 취약할 수 있다고 생각된다면, fabrication이 끝나기 직전에 데이터를 즉시 구성할 수 있습니다:

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 메서드를 사용하면 의도를 더 명확하게 표현할 수 있습니다.

이렇게 하면 셔츠를 생성한 직후에 데이터를 구성하는 것이 보장됩니다. 단점은 데이터를 사용할 필요가 없더라도 리소스가 fabricate될 때마다 항상 데이터를 구성한다는 것입니다.

대안으로, 브랜드 데이터를 구성하기 전에 올바른 페이지에 있는지 확인하는 방법도 있습니다:

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은 그대로 리소스에 저장하고, materials의 첫 번째 항목의 첫 번째 값을 main_fabric 속성으로 가져오고 싶을 수 있습니다.

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! 메서드를 호출하거나, factory를 사용하여 생성할 수 있습니다. 리소스 클래스가 API fabrication을 지원하는 경우, 기본적으로 API fabrication을 사용한다는 점에 유의하세요.

다음은 Shirt 리소스 클래스가 지원하기 때문에 내부적으로 API fabrication 메서드를 사용하는 예시입니다:

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 fabrication 메서드를 명시적으로 사용하려면 .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 fabrication 메서드를 명시적으로 사용할 수도 있습니다:

my_shirt = Resource::Shirt.fabricate_via_api! do |shirt|
  shirt.name = 'my-shirt'
end

이 경우, 결과는 Resource::Shirt.fabricate!를 호출하는 것과 유사합니다.

Factories#

테스트 내에서 리소스를 생성, 빌드, 가져오기 위해 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) }

# create one commit in a project that performs three actions
let(:commit) do
  create(:commit, commit_message: 'my message', project: project, actions: [
    { action: 'create', file_path: 'README.md', content: '# Welcome!' },
    { action: 'update', file_path: 'README.md', content: '# Updated' },
    { action: 'delete', file_path: 'README.md' }
  ])
end

###

# instantiate an Issue but don't create it via API yet
let(:issue) { build(:issue) }

# instantiate a Project and perform some actions before creating
let(:project) do
  build(:project) do |p|
    p.name = 'Test'
    p.add_name_uuid = false
  end
end

# fetch an existing issue via the API with attributes
let(:existing_issue) { build(:issue, project: project, iid: issue.iid).reload! }

모든 factory는 qa/qa/factories에 정의되어 있으며, 각각의 QA::Resource::Base 클래스를 대표합니다.

예를 들어, factory :issueqa/resource/issue.rb에서 찾을 수 있습니다. factory :projectqa/resource/project.rb에서 찾을 수 있습니다.

새 Factory 생성하기#

다음과 같은 리소스가 있다고 가정합니다:

# 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

기본값과 재정의가 있는 factory를 정의합니다:

# 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') } #
let(:named_shirt) { create(:shirt, :with_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

리소스 정리#

테스트 실행 중에 생성된 모든 리소스를 수집하는 메커니즘과, 이러한 리소스를 처리하는 메커니즘이 있습니다. dotcom 환경에서는 E2E 파이프라인에서 테스트 스위트가 완료된 후, 통과한 모든 테스트의 리소스가 동일한 파이프라인 실행에서 자동으로 삭제됩니다. 실패한 모든 테스트의 리소스는 조사를 위해 보존되며, 예약된 파이프라인에 의해 다음 토요일까지 삭제되지 않습니다. 새 리소스를 도입할 때는 삭제할 수 없는 리소스를 IGNORED_RESOURCES 목록에 추가해야 합니다.

도움을 받을 수 있는 곳은?#

더 많은 정보가 필요하다면 Slack의 #s_developer_experience 채널에서 도움을 요청하세요(내부용, GitLab 팀 전용).

GitLab 팀 멤버가 아니지만 기여를 위해 도움이 필요하다면, ~QA 라벨을 붙여 GitLab 이슈 트래커에 이슈를 열어 주세요.