GitLab QA에서의 리소스 클래스

리소스는 주로 브라우저 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!
        # ... 이전과 동일
      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!
        // ... 이전과 동일
      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

모든 속성이 지연 생성되는 것에 유의하십시오. 특정 속성을 먼저 생성하려면 해당 속성 메서드를 사용해야 합니다.

제품 데이터 속성

생성된 후에는 웹 페이지나 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

      # 브라우저 UI에서 생성된 속성(블록 사용)
      attribute :brand do
        Page::Shirt::Show.perform do |shirt_show|
          shirt_show.fetch_brand_from_page
        end
      end

      // ... 이전과 동일
    end
  end
end

다시 한번 모든 속성이 지연 생성되는 것에 유의하십시오. 페이지 이동 후에 shirt.brand을 호출하면 데이터를 올바르게 가져오지 못하기 때문입니다.


UPDATE: The translations of technical term “Page” were revised to align with the standard terminology used in the context.

API 응답을 기반으로 속성 정의하기

가끔은 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
      # ... 이전과 동일

      # @style은 인스턴스에서 사용하고자 하는 경우 사용하고,
      # API 응답에 포함되어 있는 경우 해당 값을 가져오고,
      # 그 외의 경우 QA::Resource::Base::NoValueError을 발생시킵니다
      attribute :style

      # @main_fabric이 없는 경우 API가 이 필드를 포함하고 있지 않으면
      # 이 블록은 API 응답을 기반으로 값을 구성하여 @main_fabric에 저장합니다
      attribute :main_fabric do
        api_response.&dig(:materials, 0, 0)
      end

      # ... 이전과 동일
    end
  end
end

속성 우선순위에 대한 주의사항:

  • 리소스 인스턴스 변수가 가장 높은 우선순위를 갖습니다
  • API 응답에서 가져온 속성이 블록에서 가져온 속성보다 우선합니다 (일반적으로 브라우저 UI에서 가져옴)
  • 값을 갖지 않은 속성은 QA::Resource::Base::NoValueError 오류를 발생시킵니다

테스트에서 리소스 생성하기

테스트에서 리소스를 생성하려면 리소스 클래스에서 .fabricate! 메서드를 호출하거나 factory를 사용하여 생성할 수 있습니다. 리소스 클래스가 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" 반환
expect(page).to have_text(my_shirt.brand) # => API 응답에서 가져온 "a-brand-new-brand" 반환
expect(page).to have_text(my_shirt.style) # => API 응답에서 가져온 "t-shirt" 반환
expect(page).to have_text(my_shirt.main_fabric) # => 블록을 통해 API 응답에서 가져온 "cotton" 반환

명시적으로 브라우저 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" 반환
expect(page).to have_text(my_shirt.brand) # => 블록을 통해 `Page::Shirt::Show` 페이지에서 가져온 브랜드 이름 반환
expect(page).to have_text(my_shirt.style) # => API 응답이나 블록 값이 제공되지 않아 `QA::Resource::Base::NoValueError`가 발생
expect(page).to have_text(my_shirt.main_fabric) # => API 응답이 없고 블록도 값이 제공되지 않아 `QA::Resource::Base::NoValueError`가 발생

또한 API 생성 방법을 명시적으로 사용하려면 .fabricate_via_api! 메서드를 호출할 수 있습니다.

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

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

팩토리

테스트 내에서 리소스를 생성, 빌드 및 가져오기 위해 FactoryBot을 사용할 수도 있습니다.

# 테스트에 사용할 프로젝트를 API를 통해 생성합니다
let(:project) { create(:project) }

# 프로젝트에 속한 이슈를 API를 통해 생성합니다
let(:issue) { create(:issue, project: project) }

# 특정 이름을 가진 프라이빗 프로젝트를 API를 통해 생성합니다
let(:project) { create(:project, :private, name: 'my-project-name', add_name_uuid: false) }

# 세 가지 액션을 수행하는 프로젝트에 커밋을 하나 생성합니다
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

#

Issue를 생성하지만 아직 API를 통해 만들지는 않습니다

let(:issue) { build(:issue) }

Project를 생성하여 일부 작업을 수행한 후에 만듭니다

let(:project) do build(:project) do |p| p.name = ‘테스트’ p.add_name_uuid = false end end

속성을 가진 기존 Issue를 API를 통해 가져옵니다

let(:existing_issue) { build(:issue, project: project, iid: issue.iid).reload! } ```

모든 팩토리는 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

기본값 및 오버라이드가 있는 factory 정의:

# qa/factories/shirts.rb
module QA
  FactoryBot.define do
    factory :shirt, class: 'QA::Resource::Shirt' do
      brand { '브랜드명' }

      trait :with_name do
        name { '셔츠 이름' }
      end
    end
  end
end

테스트에서 API를 통해 리소스 만들기:

let(:my_shirt) { create(:shirt, brand: '다른브랜드') } #<Resource::Shirt @brand="다른브랜드" @name=nil>
let(:named_shirt) { create(:shirt, :with_name) } #<Resource::Shirt @brand="브랜드명" @name="셔츠 이름">
let(:invalid_shirt) { create(:shirt, read_only: true) } # NoMethodError

it '셔츠를 만듭니다' do
  expect(my_shirt.brand).to eq('다른브랜드')
  expect(named_shirt.name).to eq('셔츠 이름')
  expect(invalid_shirt).to raise_error(NoMethodError) # Resource::Shirt#read_only=을 호출하려고 시도합니다
end

리소스 정리

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

도움을 요청할 곳은 어디인가요?

더 많은 정보가 필요한 경우, 내부적으로 #test-platform 채널에서 도움을 요청하세요.

만약 팀 멤버가 아니거나 기여하는 데 도움이 필요한 경우, ~QA 라벨이 있는 GitLab CE 이슈 트래커에 이슈를 열어주세요.