GitLab QA의 자원 클래스
자원은 주로 브라우저 UI 단계를 통해 생성되지만, API 또는 CLI를 통해 생성할 수도 있습니다.
자원 클래스를 올바르게 구현하는 방법은?
모든 자원 클래스는 Resource::Base
를 상속해야 합니다.
자원 클래스를 정의하는 데 구현해야 하는 필수 메소드는 단 하나뿐입니다. 이것은 브라우저 UI를 통해 자원을 빌드하는 #fabricate!
메소드입니다. 이 메소드에서는 웹 페이지와 상호 작용하기 위해 반드시 페이지 객체를 사용해야 합니다.
다음은 가상의 예시입니다.
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
본문 (루비 해시 형태)
많은 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!
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) 구성된다는 것에 주의하세요. 특정 속성을 먼저 생성하려면 사용하지 않더라도 먼저 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
# 브라우저 UI에서 채우는 속성 (블록 사용)
attribute :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
# ... 이전과 동일
end
end
end
다시 한 번, 모든 속성이 느리게 구성된다는 점에 주의하세요. 다른 페이지로 이동한 후에 shirt.brand
을 호출하면 데이터를 제대로 검색하지 못합니다.
다음을 고려해보세요.
shirt =
QA::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.project.visit!
shirt.brand # => 실패!
위의 예제는 이제 프로젝트 페이지에 있으며, 이미 셔츠 페이지로 이동한 상태에서 셔츠 데이터를 생성하려 하기 때문에 실패합니다. 이 문제를 해결하기 위한 두 가지 방법이 있습니다. 하나는 프로젝트를 다시 방문하기 전에 브랜드를 검색할 수 있도록 하는 것입니다.
shirt =
QA::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.brand # => 성공!
shirt.project.visit!
shirt.brand # => 성공!
속성은 인스턴스에 저장되므로 이후 모든 호출은 이전에 생성된 데이터를 사용합니다. 이것이 너무 취약할 수 있다고 생각된다면, 프로덕션을 끝내기 전에 데이터를 즉시 구성할 수도 있습니다.
module QA
module Resource
class Shirt < Base
# ... 이전과 동일
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) # 데이터를 즉시 구성
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
# 브라우저 UI에서 채우는 속성 (블록 사용)
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
# ... 이전과 동일
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
# ... 이전과 동일
# 인스턴스에서 @style를 가져오거나,
# API 응답에서 가져오는 속성이 없다면, 이 블록을 사용하여
# API 응답을 기반으로 값을 생성하고 @main_fabric에 결과를 저장합니다
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!
메서드를 호출하거나 팩토리를 사용하여 만들 수 있습니다. 리소스 클래스가 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!
를 호출한 것과 유사합니다.
Factories
테스트 내에서 리소스를 만들고 빌드하고 가져오기 위해 FactoryBot 호출을 사용할 수 있습니다.
# 테스트에서 사용하기 위해 API를 통해 프로젝트 생성
let(:project) { create(:project) }
# 테스트에서 사용하기 위해 프로젝트에 속하는 이슈 생성
let(:issue) { create(:issue, project: project) }
# 이름과 UUID를 추가하지 않은 특정 이름의 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
###
# API를 통해 아직 생성하지 않은 이슈 인스턴스화
let(:issue) { build(:issue) }
# 생성하기 전에 몇 가지 작업을 수행하는 프로젝트 인스턴스화
let(:project) do
build(:project) do |p|
p.name = 'Test'
p.add_name_uuid = false
end
end
# 속성을 포함하여 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
기본값 및 오버라이드로 팩토리 정의:
# 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) # Resource::Shirt#read_only=를 호출하려고 시도함
end
리소스 정리
테스트 실행 중에 생성된 모든 리소스를 수집하는 메커니즘과 이러한 리소스를 처리하는 메커니즘이 있습니다. dotcom 환경에서 QA 파이프라인에서 테스트 스위트가 완료되면, 성공한 테스트에서 생성된 리소스는 자동으로 동일한 파이프라인 실행에서 삭제됩니다. 실패한 테스트에서 생성된 리소스는 조사를 위해 예약되며, 예약된 파이프라인에 의해 다음 토요일까지 삭제되지 않습니다. 새 리소스를 도입할 때, 삭제할 수 없는 리소스를 IGNORED_RESOURCES 디렉터리에 추가하는 것도 잊지 마세요.
도움을 요청할 곳
더 많은 정보가 필요하다면 내부 GitLab 팀의 Slack #test-platform
채널에서 도움을 요청하세요.
팀 멤버가 아니고 기여에 도움이 필요한 경우, ~QA
라벨이 달린 GitLab CE 이슈 트래커에 이슈를 열어 도움을 요청하세요.