- 왜 페이지 오브젝트가 필요한가요?
- 과거에 어떤 문제가 있었나요?
- 불안정한 테스트 문제를 어떻게 해결했나요?
- 페이지 오브젝트를 적절하게 구현하는 방법은?
- 로컬에서 테스트 실행
- 도움을 요청할 곳은?
GitLab QA의 페이지 오브젝트
GitLab QA에서는 _Page Objects_라는 알려진 패턴을 사용합니다.
이는 GitLab의 모든 페이지에 대한 추상화를 구축하여 GitLab QA 시나리오를 수행하는 데 사용하는 것을 의미합니다. 페이지에서 어떤 작업을 수행할 때(예: 양식 작성 또는 버튼 선택), GitLab의 이 영역과 관련된 페이지 오브젝트를 통해서만 수행합니다.
예를 들어, GitLab QA 테스트 하네스가 GitLab에 로그인할 때 사용자 로그인 및 사용자 비밀번호를 입력해야 합니다. 이를 위해 Page::Main::Login
이라는 클래스와 sign_in_using_credentials
메서드가 있는데, 이것은 user_login
및 user_password
필드를 읽는 유일한 코드 조각입니다.
왜 페이지 오브젝트가 필요한가요?
페이지 오브젝트가 필요한 이유는 중복을 줄이고 GitLab 소스 코드의 선택기를 수정할 때 문제를 피하기 위해서입니다.
GitLab QA에 100여 개의 스펙이 있다고 가정해보세요. 우리는 단언을 하기 전에 매번 GitLab에 로그인해야 합니다. 페이지 오브젝트가 없으면 불안정한 도우미에 의존하거나 직접 Capybara 메서드를 호출해야 합니다. 매 *_spec.rb
파일 / 테스트 예제에서 fill_in :user_login
을 호출한다고 상상해보세요.
나중에 누군가가 이 페이지와 관련된 뷰에서 t.text_field :login
을 t.text_field :username
으로 변경하면 다른 필드 식별자가 생성되어 모든 테스트에 영향을 미칩니다.
우리는 여기저기서 Page::Main::Login.perform(&:sign_in_using_credentials)
을 사용하여 GitLab에 로그인하고자 합니다. GitLab에 로그인하려면 이 페이지 오브젝트는 단일한 진실의 원천이며, fill_in :user_login
을 fill_in :user_username
로 변경해야 하는 곳은 한 곳 뿐입니다.
과거에 어떤 문제가 있었나요?
성능 문제로 인해 모든 커밋에 대해 QA 테스트를 실행하지 않기 때문에 모든 것을 빌드하고 테스트하기에는 시간이 오래 걸립니다.
이것이 바로 누군가가 t.text_field :login
을 t.text_field :username
로 변경할 때 우리가 새로운 세션 뷰에서 이 변경 사항을 알지 못하는 이유입니다. 그래서 우리는 GitLab QA 내부 파이프라인이 실패할 때까지 또는 누군가가 자신의 Merge Request에서 package-and-qa
액션을 트리거할 때까지 이 변경 사항을 알지 못합니다.
이러한 변경 사항은 모든 테스트를 깨뜨립니다. 우리는 이 문제를 _불안정한 테스트 문제_라고 부릅니다.
GitLab QA를 더 신뢰할 수 있고 견고하게 만들기 위해, GitLab CE / EE 뷰와 GitLab QA 사이의 결합을 도입하여 이 문제를 해결해야 했습니다.
불안정한 테스트 문제를 어떻게 해결했나요?
현재, 새로운 Page::Base
파생 클래스를 추가할 때 이 페이지 오브젝트가 의존하는 모든 선택기를 정의해야 합니다.
CE / EE 리포지터리에 코드를 푸시할 때마다 CI 파이프라인의 일부로 qa:selectors
세심성 테스트 작업이 실행됩니다.
이 테스트는 qa/page
디렉터리에 구현된 모든 페이지 오브젝트를 유효성 검사합니다. 이 테스트가 실패하면 뷰/선택기 정의가 누락되었거나 잘못되었음을 알립니다.
페이지 오브젝트를 적절하게 구현하는 방법은?
페이지 오브젝트와 실제로 구현된 GitLab 뷰 간의 결합을 정의하기 위해 DSL을 구축했습니다. 아래 예제를 참조하세요.
module Page
module Main
class Login < Page::Base
view 'app/views/devise/passwords/edit.html.haml' do
element :password_field
element :password_confirmation
element :change_password_button
end
view 'app/views/devise/sessions/_new_base.html.haml' do
element :login_field
element :password_field
element :sign_in_button
end
# ...
end
end
end
요소 정의
view
DSL 메서드는 요소를 렌더링하는 Rails 뷰, 부분 또는 Vue 컴포넌트에 해당합니다.
그리고 element
DSL 메서드는 해당 요소에 대응하는 testid=element_name
데이터 속성을 뷰 파일에 추가해야 한다는 것을 선언합니다.
또한 실제 뷰 코드와 일치하는 값을(문자열 또는 정규식) 정의할 수도 있지만 이것은 폐기되었으며 아래의 이유로 권장되지 않습니다:
- 일관성: 요소를 정의하는 유일한 방법이 있습니다.
- 관심사 분리: QA는 다른 컴포넌트에서 사용되는 코드 또는 클래스를 재사용하는 대신 전용
data-qa-*
속성을 사용합니다(예:js-*
클래스 등).
view 'app/views/my/view.html.haml' do
### 좋음 ###
# 뷰에 `[data-testid="logout_button"]` CSS 선택기가 있어야 함을 암시적으로 요구
element :logout_button
### 나쁨 ###
## 이것은 폐기되었으며 `QA/ElementWithPattern` RuboCop cop에 의해 금지됩니다.
# `my/view.html.haml`에서 `f.submit "Sign in"`이 있어야 함을 요구
element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern
## 이것은 폐기되었으며 `QA/ElementWithPattern` RuboCop cop에 의해 금지됩니다.
# `my/view.html.haml`의 모든 줄을 `/link_to .* "My Profile"/` 정규식과 일치시킴
element :profile_link, /link_to .* "My Profile"/ # rubocop:disable QA/ElementWithPattern
end
뷰에 요소 추가
다음 요소가 있다고 가정해봅시다…
view 'app/views/my/view.html.haml' do
element :login_field
element :password_field
element :sign_in_button
end
이러한 요소를 뷰에 추가하려면 각 정의된 요소에 대해 Rails 뷰, 부분 또는 Vue 컴포넌트를 변경하여 data-testid
속성을 추가해야 합니다.
우리의 경우, data-testid="login_field"
, data-testid="password_field"
, data-testid="sign_in_button"
app/views/my/view.html.haml
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { testid: 'login_field' }
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { testid: 'password_field' }
= f.submit "Sign in", class: "btn btn-confirm", data: { testid: 'sign_in_button' }
주의할 점:
- 요소의 이름과
data-testid
는 일치해야 하며, 스네이크 케이스 또는 케밥 케이스여야 합니다. - 요소가 조건 없이 페이지에 나타나는 경우, 요소에
required: true
를 추가해야 합니다. 동적 요소 유효성 검사 참조 - 페이지 오브젝트에
data-qa-selector
클래스가 표시되지 않아야 합니다. 우리는 정의를 위해data-testid
방법을 사용해야 합니다.
data-testid
대 data-qa-selector
- GitLab 16.1에 도입됨
기존 data-qa-selector
클래스는 폐기될 것으로 간주되며,
정의하는 방법으로 data-testid
방법을 사용해야 합니다.
동적 요소 선택
- GitLab 12.5에 도입됨
자동화된 테스트에서 흔히 발생하는 경우는 “하나 중의 하나” 요소를 선택하는 것입니다. 여러 항목의 디렉터리에서 어떻게 선택할지 구분해야 하는 것입니다. 가장 흔한 해결책은 텍스트 일치를 통해 수행됩니다. 대신에 더 나은 방법은 텍스트가 아닌 고유 식별자에 따라 해당 요소를 선택하는 것입니다.
우리는 data-qa-*
확장 선택 메커니즘을 추가하여 이를 해결했습니다.
예시
예시 1
다음 Rails 뷰가 주어졌다고 가정합니다 (GitLab 이슈를 사용한 예시):
%ul.issues-list
- @issues.each do |issue|
%li.issue{data: { testid: 'issue', qa_issue_title: issue.title } }= link_to issue
우리는 Rails 모델과 일치시켜 특정 이슈를 선택할 수 있습니다.
class Page::Project::Issues::Index < Page::Base
def has_issue?(issue)
has_element?(:issue, issue_title: issue)
end
end
우리의 테스트에서 특정 이슈가 존재하는지 확인할 수 있습니다.
describe 'Issue' do
it 'has an issue titled "hello"' do
Page::Project::Issues::Index.perform do |index|
expect(index).to have_issue('hello')
end
end
end
예시 2
인덱스로…
%ol
- @some_model.each_with_index do |model, idx|
%li.model{ data: { testid: 'model', qa_index: idx } }
expect(the_page).to have_element(:model, index: 1) #=> 디렉터리에서 첫 번째 모델을 선택합니다
예외
일부 경우에는 선택기를 추가하는 것이 불가능하거나 가치가 없을 수 있습니다.
일부 UI 컴포넌트는 서드 파티가 유지 관리하는 외부 라이브러리를 사용합니다. 라이브러리가 GitLab에서 유지 관리되더라도, 선택기 검사는 GitLab 프로젝트 내의 코드에서만 실행되므로 라이브러리의 코드를 위한 뷰 경로를 지정할 수 없습니다.
이러한 드문 경우에는 페이지 객체 메서드에서 CSS 선택기를 사용하고, 선택기를 추가할 수 없는 이유에 대해 설명하는 주석을 추가하는 것이 합리적입니다.
페이지 관심 사항 정의
일부 페이지는 일반 동작을 공유하거나/또는 EE(Enterprise Edition)별 메서드를 추가하는 가능성이 있습니다.
이러한 모듈은 다음을 해야 합니다:
-
QA::Page::PageConcern
모듈에서 확장되어야 합니다.extend QA::Page::PageConcern
로 확장됩니다. -
self.prepended
메서드를 오버라이드하고, 자체적으로include
/prepend
를 해야 하거나,view
또는elements
를 정의해야 하면 오버라이드해야 합니다.self.prepended
에서 가장 먼저super
를 호출해야 합니다. - 다른 모듈을 포함하거나,
view
/elements
를 정의하고, 이러한 모듈이 클래스에 정의되도록 하기 위해base.class_eval
블록에서 이들을 포함/정의해야 합니다.
위의 단계에 따라 문제를 적절하게 감지할 수 있도록 합니다.
예를 들어, qa/qa/ee/page/merge_request/show.rb
은 qa/qa/page/merge_request/show.rb
에 EE-별 메서드를 추가합니다
(QA::Page::MergeRequest::Show.prepend_mod_with('Page::MergeRequest::Show', namespace: QA)
로 추가) 그리고 다음은
그것이 구현된 방식입니다(관련 부분만 표시하고 위에서 설명한 4 단계를 인라인 주석으로 참조함):
module QA
module EE
module Page
module MergeRequest
module Show
extend QA::Page::PageConcern # 1.
def self.prepended(base) # 2.
super # 3.
base.class_eval do # 4.
prepend Page::Component::LicenseManagement
view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do
element :head_mismatch, "The source branch HEAD has recently changed."
end
[...]
end
end
end
end
end
end
end
로컬에서 테스트 실행
개발 중에 qa
디렉터리에서 다음을 실행하여 qa:selectors
테스트를 실행할 수 있습니다.
bin/qa Test::Sanity::Selectors
도움을 요청할 곳은?
더 많은 정보가 필요한 경우, Slack의 #test-platform
채널에서 도움을 요청하십시오
(내부, GitLab팀 전용).
팀 멤버가 아니라도 기여에 도움이 필요한 경우에는 GitLab CE 이슈 트래커에 ~QA
라벨이 달린 이슈를 개설하십시오.