반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 받은 트랙백

글 보관함

Selenium WebDriver - PageObjects

2013. 10. 19. 10:53 | Posted by 솔웅


PageObjects는 Selenium WebDriver 테스트에서 권장하는 디자인 패턴입니다.

반복되는 코드를 패턴화 해서 하나로 만들어 놓고 활용하는 방법입니다.

제가 충분히 이해를 못 했는지 원문이 해석하기 무척 힘드네요. 일단 Tutorial 을 한번 대강 해석해 놓고 나중에 소스를 좀 분석해 보겠습니다.


==================================

PageObjects 



Page Object 패턴은 객체들의 시리즈로서 여러분의 웹앱 화면들을 represent 합니다.


WebDriver



Updated Mar 31, 2013




Page Objects

웹앱 UI에서 어떤 테스트해야할 부분이 있습니다. Page Object는 이러한 테스트해야할 부분들을 테스트 코드내에서 한 객체로서 간단하게 다룹니다. 이것은 반복되는 코드를 줄이고 ,UI가 바뀌었을 때 한 부분만 고치면 되도록 만듭니다.


Implementation Notes

PageObject는 동시에 두가지 방향을 생각하게 할 수 있습니다. 우선 한 방향은 테스트를 하는 개발자에게 향하는 것입니다. 특정 페이지에 의해 제공되는 서비스가 됩니다. 두번째는 개발자로부터 나오는 방향입니다. HTML 페이지의 구조에 대한 깊은 이해가 있어야 합니다. Page Object는 페이지가 요구하는 그런 서비스의요구로서 생각됩니다. 예를 들어 웹베이스 이메일 시스템의 inbox를 생각해 봅시다. 여기서 제공하는 기능들은 편지를 쓰거나 어떤 이메일을 읽거나 inbox 내 이메일들의 리스트를 보여주는 겁니다. 이러한 것들이 테스트를 하는데 있어서 어떻게 implement 될까요.



우리는 개발자에게 implementation 하는것 보다 interaction 하는 그런 서비스들에 대해 좀 더 생각하도록 해야하기 때문에 PageObject는 WebDriver 인스턴스의 기본적인 부분들 같은 것들을 그렇게 드러내지 않습니다. 이것을 구현하기 위해 PageObject의 메소드들은 다른 PageObject를 return 해야 합니다. 이 의미는 우리 어플리케이션을 통해 유저의 사용경험들을 효과적으로 모델화 할 수 있다는 겁니다. It also means that should the way that pages relate to one another change (like when the login page asks the user to change their password the first time they log into a service, when it previously didn't do that) simply changing the appropriate method's signature will cause the tests to fail to compile. Put another way, we can tell which tests would fail without needing to run them when we change the relationship between pages and reflect this in the PageObjects.



이 접근법의 결과로는 성공적인 로그인과 성공적이지 않은 로그인 보두에 대한 모델링이 필요하다는 겁니다. 혹은 앱의 상황에 따라서 click 의 결과가 다르게 나올 수 있다는 것이구요. PageObject에 여러 메소드들을 갖는것은 일반적인 것입니다.



public class LoginPage {
    public HomePage loginAs(String username, String password) {
        // ... clever magic happens here
    }
   
    public LoginPage loginAsExpectingError(String username, String password) {
        //  ... failed login here, maybe because one or both of the username and password are wrong
    }
   
    public String getErrorMessage() {
        // So we can verify that the correct error is shown
    }
}



위의 코드를 보면 아주 중요한 부분이 있습니다. 이 테스트들은 PageObjects가 아닙니다. 페이지의 상태에 대한 assertion들을 만들어야 합니다. 예를 들어



public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    inbox.assertMessageWithSubjectIsUnread("I like cheese");
    inbox.assertMessageWithSubjectIsNotUnread("I'm not fond of tofu");
}



이 코드는 아래와 같이 재 작성해야 합니다.



public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
    assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}




물론 모든 guideline에는 예외가 있습니다. 그중에 PageObjects에서 흔히 볼 수 있는 것이 우리가 PageObject를 초기화 할 때 맞는 페이지에서 WebDriver가 있는지에 대해 체크하는 것입니다. 이 작업은 아래 예제에서와 같이 처리하시면 됩니다.


마지막으로 PageObject는 모든 페이지를 represent 할 필요는 없습니다. PageObjects는 그 사이트나 페이지 내에서 여러번 반복되는 부분을 represent 하면 됩니다. 여기서 핵심은 특정 페이지의 HTML 구조에 대한 지식을 기반으로 여러분의 test suite 내에서 한 부분에서 이것을 만들어 두면 된다는 겁니다.


Summary

    The public methods represent the services that the page offers
    페이지에서 요구되는 서비스들을 represent 하는 public methods
    Try not to expose the internals of the page
    페이지의 내부적인 부분을 표현하려고 하지 않는다.
    Generally don't make assertions
    일반적으로 assertion들을 만들지 않는다.
    Methods return other PageObjects
    다른 PageObjects를 return 한다.
    Need not represent an entire page
    전체 페이지를 represent 할 필요는 없다.
    Different results for the same action are modelled as different methods
    같은 action에 대한 다른 결과들을 위해 다른 메소드들로 모델링 한다.




Example



public class LoginPage {
    private final WebDriver driver;

    public LoginPage(WebDriver driver) {
        this.driver = driver;

        // Check that we're on the right page.
        if (!"Login".equals(driver.getTitle())) {
            // Alternatively, we could navigate to the login page, perhaps logging out first
            throw new IllegalStateException("This is not the login page");
        }
    }

    // The login page contains several HTML elements that will be represented as WebElements.
    // The locators for these elements should only be defined once.
        By usernameLocator = By.id("username");
        By passwordLocator = By.id("passwd");
        By loginButtonLocator = By.id("login");

    // The login page allows the user to type their username into the username field
    public LoginPage typeUsername(String username) {
        // This is the only place that "knows" how to enter a username
        driver.findElement(usernameLocator).sendKeys(username);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;   
    }

    // The login page allows the user to type their password into the password field
    public LoginPage typePassword(String password) {
        // This is the only place that "knows" how to enter a password
        driver.findElement(passwordLocator).sendKeys(password);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;   
    }

    // The login page allows the user to submit the login form
    public HomePage submitLogin() {
        // This is the only place that submits the login form and expects the destination to be the home page.
        // A seperate method should be created for the instance of clicking login whilst expecting a login failure.
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the login page ever
        // go somewhere else (for example, a legal disclaimer) then changing the method signature
        // for this method will mean that all tests that rely on this behaviour won't compile.
        return new HomePage(driver);   
    }

    // The login page allows the user to submit the login form knowing that an invalid username and / or password were entered
    public LoginPage submitLoginExpectingFailure() {
        // This is the only place that submits the login form and expects the destination to be the login page due to login failure.
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the user ever be navigated to the home page after submiting a login with credentials
        // expected to fail login, the script will fail when it attempts to instantiate the LoginPage PageObject.
        return new LoginPage(driver);  
    }

    // Conceptually, the login page offers the user the service of being able to "log into"
    // the application using a user name and password.
    public HomePage loginAs(String username, String password) {
        // The PageObject methods that enter username, password & submit login have already defined and should not be repeated here.
        typeUsername(username);
        typePassword(password);
        return submitLogin();
    }
}


Support in WebDriver

이 패턴을 지원하는 패키지로 PageFactory 가 있습니다. 이것은 PageObjects에서 boiler-plate 코드를 동시에 remove 하는 것을 도와 줍니다.


=================================================


사실 이 글을 읽고는 무엇인지 확실히 이해를 잘 못하겠구요. 그나마 저 소스코드를 보니까 대강 알 수 있을 것 같습니다.


이 LoginPage 클래스에서는 우선 생성자에서 여기가 login 페이지가 맞는지 확인하고 해당 페이지가 아니면 예외를 발생 시키네요.

이 클래스는 로그인 페이지를 위한 클래스 입니다.


그리고 By type의 객체 세개를 만들었는데 각각 id로 유저명, 비밀번호 그리고 login 버튼을 찾는 객체들입니다.


첫번째 메소드는 typeUsername 인데요. 여기서는 LoginPage를 return 하네요.

PageObjects는 다른 PageObjects를 return 한다던데 이것을 말하나 봅니다.

그 안에는 by id 로 유저명을 찾아서 거기다가 전달 받은 파라미터를 type 해 넣습니다.


두번째 메소드인 typePassword도 똑 같습니다. 여기는 비밀번호를 쳐 넣는겁니다.


그 다음 메소드인 submitLogin 이 나오는데요. 

여기서는 HomePage를 return 하네요.

메소드 내에서는 login button을 클릭하는 일을 하구요.


id하고 비밀번호를 입력하고 login 버튼을 클릭하게 되겠죠.


그 다음 메소드는 이전 메소드와 완전히 똑 같습니다. 단지 이름이 submitLoginExpectingFailure() 인걸로 봐서 fail이 예상될 때 호출하는 메소드 인가 봅니다.


그 다음 loginAs() 메소드가 실제 실행할 때 호출하는 메소드인데요.

username과 password를 파라미터로 받아서 typeUsername()과 typePassword() 를 호출해서 아이디와 비밀번호를 타입하고 return submitLogin() 함으로서 로그인 버튼을 클릭하고 성공한 페이지를 return 하는 일을 합니다.

이렇게 하면 로그인 기능은 어디서든지 불러서 사용할 수 있겠네요.


그러면 이 부분에 대한 반복되는 코딩을 막을 수 있구요.


아이디, 비밀번호 이외에 다른 정보가 들어가는 걸로 바뀌거나 아이디에 대한 룰이 바뀌거나 하면 이 부분만 바꿔주면 되구요.


이 패턴을 지원하는 패키지가 PageFactory 라고 하니까 다음 글에서는 이걸 한번 배워봐야겠습니다.


반응형

Comment