понедельник, 19 июля 2010 г.

PageObjects pattern + Selenium (Java)

Попробуем на небольшом примере показать как, используя PageObjects pattern и Selenium (Java), можно достаточно быстро реализовать фреймоворк для автоматизированного тестирования, и начать писать понятные, легко поддерживаемые, а главное работающие тесты.

Пример:

Страница LoginPage состоит из двух полей ввода User и Password, кнопки Login. При отправке корректного логина и пароля открывается страница Home на которой есть имя пользователя и ссылка Logout. При отправке НЕкорректного логина и пароля открывается страница ErrorLogin, на которой есть сообщение об ошибке и ссылка Back To Login Page.


Задание:

Написать тестовые скрипты:
1. Корректный Login -> проверка имени пользователя -> Logout
2. Некорректный Login -> проверка сообщения об ошибке -> Back To Login Page


Решение:

1. Создаем рабочий контекст на базе Selenium

Context.java:
public class Context {
    public static final String BROWSER_IE = "*iexplore";
    public static final String BROWSER_FF = "*firefox";
    public static final String BROWSER_CH = "*chrome";
    
    private static Context context;
    private static String siteUrl;
   
    private Selenium browser;
    private SeleniumServer seleniumServer;

    private Context() {
    }

    public static void initInstance(String browserType, String siteURL) {
        context = new Context();
        siteUrl = siteURL;
        context.setBrowser(new DefaultSelenium("localhost", 4444, browserType, siteURL));
        context.start();
    }

    public static Context getInstance() {
        if (context == null) {
            throw new IllegalStateException("Context is not initialized");
        }
        return context;
    }

    public Selenium getBrowser() {
        if (browser != null) {
            return browser;
        }
        throw new IllegalStateException("WebBrowser is not initialized");
    }
    
    public String getSiteUrl() {
       return siteUrl;
    }

    public void start() {
        try {
            seleniumServer = new SeleniumServer();
            seleniumServer.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
        browser.start();
    }
    
    public void close() {
        browser.close();
        browser.stop();
        seleniumServer.stop();
    }
        
    public void setBrowser(Selenium browser) {
        this.browser = browser;
    }
}

2. Добавляем в абстрактный суперкласс Page ссылку на рабочий контекст context, а также сервисный метод для получения из него объекта Selenium getBrowser().


Page.java:
public abstract class Page {
    private Context context;
    private String currentPage; 
    
    protected Page(String pageUrl) {
        this.currentPage = pageUrl;
        setContext(Context.getInstance());
        init();
        parsePage();
    }

    private void setContext(Context instance) {
        this.context = instance;
    }

    protected abstract void init();
    protected abstract void parsePage();

    public String getCurrentPage() {
  return context.getSiteUrl() + this.currentPage;
    }
   
    protected Selenium getBrowser() {
        return context.getBrowser();
    }

    // ....
    // service methods...
    // ....
}

3. Реализуем классы страниц, необходимые при написании тестов для нашего примера, а именно: LoginPage, HomePage и ErrorLoginPage. И добавляем в сервисные методы реальный ввод данных в поля, нажатие кнопок и клики на ссылки


LoginPage.java:
public class LoginPage extends Page {
   public static final String PAGE_URL = "/login.html";

   protected LoginPage() {
       super(PAGE_URL);
   }

   public static LoginPage openLoginPage() {
       LoginPage loginPage = new LoginPage();
       loginPage.getBrowser().open(PAGE_URL);
       return loginPage;
   }

   private void setUserName(String userName) {
       // код для заполнения поля Username
       getBrowser().type("id=UserName", userName);
   }

   private void setPassword(String password) {
       // код для заполнения поля Password
       getBrowser().type("id=Password", password);
   }

   private void pushLoginButton() {
       // код для нажатия на кнопку Login
       getBrowser().click("id=LoginButton");
   }

   protected void parsePage() {
       // Разбор элементов страницы
       // Заполнение необходимых переменных данными со страницы
   }

   protected void init() {
       // Инициализация страницы, например проверка адреса (URL) страницы:   
       if(!getBrowser().getLocation().equals(PAGE_URL)) {
           throw new IllegalStateException("Invalid page is opened");
       }
       // Можно также добавить проверки наличия необходимых для дальнейшей работы 
       // объектов и т.д.
   }

   private void loginAs(String userName, String password) {
       setUserName(userName);
       setPassword(password);
       pushLoginButton();
   }

   public HomePage login(String userName, String password) {
       loginAs(userName, password);
       return new HomePage();
   }

   public ErrorLoginPage loginInvalid(String userName, String password) {
       loginAs(userName, password);
       return new ErrorLoginPage();
   }
}

HomePage.java:
public class HomePage extends Page {
   public static final String PAGE_URL = "/home.html";
   private String loggedinUserName;

   protected HomePage() {
       super(PAGE_URL);
   }

   protected void init() {
       // Инициализация страницы
   }

   protected void parsePage() {
       // Разбор элементов страницы
       this.loggedinUserName = getBrowser().getText("id=userName");
   }

   public String getLoggedinUserName() {
       return loggedinUserName;
   }

   public LoginPage logout() {
       getBrowser().click("id=LogoutLink");
       return new LoginPage();
   }
}

ErrorLoginPage.java:
public class ErrorLoginPage extends Page {
   public static final String PAGE_URL = "/loginError.html";
   private String errorMessage;
 
   protected ErrorLoginPage() {
       super(PAGE_URL);
   }

   protected void init() {
       // Инициализация страницы
   }

   protected void parsePage() {
       // Разбор элементов страницы
       this.errorMessage = getBrowser().getText("id=ErrorMessage");
   }

   public String getErrorMessage() {
       return this.errorMessage;
   }

   public LoginPage backToLoginPage() {
       getBrowser().click("id=BackLink");
       return new LoginPage();
   }
}

4. Пишем тесты на базе JUnit:

public class TestLogin extends TestCase {
   public void setUp() {
        // Инициализация контекста. 
        Context.initInstance(Context.BROWSER_IE, "http://www.testtesttestlogin.com");
    }

   public void testLoginLogout() {
        String userName = "tester";
        String password = "testPass";
        LoginPage loginPage = LoginPage.openLoginPage();
        HomePage homePage = loginPage.login(userName, password);
        assertEquals(userName, homePage.getLoggedinUserName());
        homePage.logout();
    }

    public void testInvalidLogin() {
        String userName = "$tester@#";
        String password = "********";
        String expectedMessage = "Invalid username or password"; 
        LoginPage loginPage = LoginPage.openLoginPage();
        ErrorLoginPage errorLoginPage = loginPage.loginInvalid(userName, password);
        assertEquals(expectedMessage, errorLoginPage.getErrorMessage());
        errorLoginPage.backToLoginPage();
    }

    protected void tearDown() throws Exception {
        // закрытие браузера
       Context.getInstance().close(); 
    }
}

Скачать исходный код примеров по PageObjectPattern

Вот все и готово - первые объекты страниц, первые тесты. Попробуйте, может и Вам подойдет подобная архитектура. В любом случае всегда все можно обсудить - любые комментарии с вопросами и конструктивной критикой приветствуются. Вопросы “отвечаются” быстро, критика анализируется... :)


Алексей Булат
Да прибудет с Вами автотестинг.


7 комментариев:

Виталий Духов комментирует...

Думаю данный фреймворк действительно очень популярен в силу своей очевидности. По крайней мере на зная таких умных слов как "PageObject Pattern" мы у себя сделали примерно тоже самое только для WinForms приложения (FormsObject pattern или как-то так видимо должен называться :) ). Видимо во времена ООП такое решение первым приходит на ум.

А.Б. комментирует...

Даже учитывая, что подход действительно популярен, информации и примеров реализации очень мало. Поэтому я и решил поделиться со всеми тем, что имею...
Для кого-то это будет уже пройденный материал, а для кого-то, надеюсь, окажется достаточно полезной информацией, позволяющей сделать шаг вперед.

На счет названия "PageObject Pattern", придумал его не я, хотя думаю именно так я бы его и назвал :)

Виталий Духов комментирует...

Поэтому я и решил поделиться со всеми тем, что имею...

И это правильно :)

Сам подумываю на примере нашей системы АТ написать доклад к SQA Days про построение и использование похожего фрэймворка для Windows приложений... (а то такое ощущение что все вокруг Вебом заняты:) )

Вдруг рецензентам понравиться, не зря ведь "Приоритет отдается докладам, которые несут практический опыт решения проблем" :)

Felix комментирует...

Алексей - а вы случаем не сталкивались с рекордерами для селениума под експлорер?

А.Б. комментирует...

К сожалению нет, да и как-то не люблю я рекордеры... :)

Unknown комментирует...

Попробовал Ваш пример ввести в Eclipse.
IDE ругается на
public static LoginPage openLoginPage() {
getBrowser().open(PAGE_URL);
return new LoginPage();
}

из класса LoginPage, говорит "Cannot make a static reference to the non-static method getBrowser() from the type Page"

Вы не подскажете как решить проблему?

А.Б. комментирует...

Vovs, большое спасибо за выявленную опытным путем багу!

метод должен выглядеть так:
public static LoginPage openLoginPage() {
LoginPage loginPage = new LoginPage();
loginPage.getBrowser().open(PAGE_URL);
return loginPage;
}


В основном посте так же было исправлено.

Еще раз спасибо!

Условия копирования публикаций:

Все публикации в данном блоге являются частной собственностью авторов. Любое копирование информации допускается только при условии указания имени автора и активной ссылки на источник.