Day-to-day the memorandum

やったことのメモ書きです。

Seleniumで問い合わせフォームをテストしてみる

今回は前回作った問い合わせフォームをSeleniumでテストしてみようと思います。


1. Spring Bootで問い合わせフォームを作ってみる
2. 1で作った問い合わせフォームをSeleniumを使ってテストしてみる ←イマココ!
3. Jenkinsを使って自動デプロイ及び自動テストできるようにする

はじめに

このテストは、URLにアクセスし、フォームに値を入れ、データを送信し、データベースに期待される値が入っているかをテストします。

準備

フォルダ構成

f:id:ZYPRESSEN:20170506104525p:plain

Gradleの設定

build.gradle

group 'spring-boot-web-test'
version '1.0-SNAPSHOT'

apply plugin: 'java'
apply plugin: 'idea'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    // https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java
    compile group: 'org.seleniumhq.selenium', name: 'selenium-java', version: '3.4.0'
    // https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-firefox-driver
    compile group: 'org.seleniumhq.selenium', name: 'selenium-firefox-driver'
    // https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-chrome-driver
    compile group: 'org.seleniumhq.selenium', name: 'selenium-chrome-driver'
    // https://mvnrepository.com/artifact/org.dbunit/dbunit
    compile group: 'org.dbunit', name: 'dbunit', version: '2.5.3'
    // https://mvnrepository.com/artifact/org.slf4j/slf4j-simple
    compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25'
    // https://mvnrepository.com/artifact/mysql/mysql-connector-java
    compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.40'
}

seleniumと一緒にwebdriverも入れています。
直接データベースをいじるのでdbunitも入れています。
あとログ関係でslf4-simpleというものを使うのでそれも一緒に入れています。

WebDriver

ブラウザを操作するライブラリー群です。
今回はChromeを使うことにしました。
以下のサイトからChromeDriverをダウンロードしてプロジェクト直下に配置します。
Downloads - ChromeDriver - WebDriver for Chrome

FirefoxDriverを使うためのgeckodriverはこちらにあります。
Releases · mozilla/geckodriver · GitHub

テストの作成

テストの実行順序

  • データベースのバックアップする。
  • テストデータを挿入する。
  • ブラウザを起動する。
  • テストを実行する。
  • スクリーンショットを撮り、ブラウザを閉じる。
  • バックアップしたデータを戻す。

練習を兼ねてJUnitの機能をたくさん使おうと思います。

Application.java

Application.java

//import省略
public class Application {
    private static String JDBC_URL = "jdbc:mysql://192.168.33.10/sample";
    private static String USER = "myapp";
    private static String PASSWORD = "myapp";
    public static String TABLE_NAME = "inquiry";

    static IDatabaseTester iDatabaseTester;
    static IDatabaseConnection iDatabaseConnection;
    private static Connection connection;

    Application() throws Exception{
    }

    @ClassRule
    public static TestRule dbRule = (statement, description) -> new Statement() {
        private File file;
        //テストを行う前処理と後処理を書くことができる。
        @Override
        public void evaluate() throws Throwable {
            dataBackUp();
            statement.evaluate(); //実際のテストを実行
            restore();
        }
        private void dataBackUp() throws Exception {
            System.out.println("TestRule databackup");
            connection = DriverManager.getConnection(JDBC_URL, USER, PASSWORD);
            iDatabaseConnection = new DatabaseConnection(connection, connection.getSchema());
            iDatabaseConnection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new MySqlDataTypeFactory());
            iDatabaseTester = new DefaultDatabaseTester(iDatabaseConnection);

            QueryDataSet partialDataSet = new QueryDataSet(iDatabaseConnection);
            partialDataSet.addTable(TABLE_NAME);
            file = File.createTempFile("prepare_backup",".xml");
            FlatXmlDataSet.write(partialDataSet, new FileOutputStream(file));
        }
        private void restore() throws Exception {
            System.out.println("TestRule restore");
            iDatabaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
            iDatabaseTester.setDataSet(new FlatXmlDataSetBuilder().build(file));
            DatabaseOperation.CLEAN_INSERT.execute(iDatabaseConnection, new FlatXmlDataSetBuilder().build(file));
        }
    };
}

オーバーライドしているevaluate()の処理にテストを実行する前や後の処理を書くことができます。
そしてstatement.evaluate()でテスト実行することになります。
dataBackUp()は今のデータベースの状態をtmpファイルに保存しています。
restore()はtmpファイルに保存したデータをデータベースに戻しています。
@ClassRuleはテストクラス単位ごとに実行されます。
なので実行するテストのクラスごとに初めにデータベースのバックアップ処理を行い、最後にデータを戻すようにしています。
DatabaseOperation.CLEAN_INSERTはデータセット(バックアップしたファイル)で指定したテーブルのデータが全て削除され、データセットのデータがDBにインサートされます。
CLEAN_INSERTだけではなくDELTE_ALLやNONEなどがあります。

ExternalRule.java

ExternalRule.java

//import省略
public class ExternalRule extends ExternalResource {
    private  TestDescription testDescription;
    private IDatabaseTester iDatabaseTester;
    private IDatabaseConnection iDatabaseConnection;
    private static String xmlPath = "datasets/fixtures/fixture.xml";

    ExternalRule(TestDescription description, IDatabaseTester iDatabaseTester, IDatabaseConnection iDatabaseConnection) throws Exception{
        this.testDescription = description;
        this.iDatabaseTester = iDatabaseTester;
        this.iDatabaseConnection = iDatabaseConnection;
    }

    @Override
    protected void before() throws Throwable {
        System.out.println("ExternalRule before");
        iDatabaseConnection.getConnection().createStatement().executeUpdate("DELETE FROM " + Application.TABLE_NAME);
        iDatabaseConnection.getConnection().createStatement().executeUpdate("ALTER TABLE " + Application.TABLE_NAME + " AUTO_INCREMENT = 1");

        DatabaseOperation.CLEAN_INSERT.execute(iDatabaseConnection, new XmlDataSet(getClass().getResourceAsStream(xmlPath)));
    }
}

ExternalResourceクラスを使うことでテスト単位ごとの前処理、後処理を書くことができます。
今回はbefore()のみ定義していて、テーブルの中身を削除して、オートインクリメントの初期化を行っています。
そのあとはデータセットに定義された値をデータベースに挿入しています。
そのデータセットresources/datasets/fixtures/fixture.xmlに記述していますが、今回は特に最初にテストデータを挿入する必要はないのでコメントアウトしています。

TestDescription.java

TestDescription.java

//import省略
public class TestDescription extends TestWatcher {
    private Description description;
    WebDriver driver;

    public Description getDescription() {
        return description;
    }

    @Override
    protected void starting(Description d) {
        description = d;
//        System.setProperty("webdriver.gecko.driver", "geckodriver.exe");
//        driver = new FirefoxDriver();
        System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
        driver = new ChromeDriver();
        System.out.println("TestDescription starting");
    }

    @Override
    protected void finished(Description description) {
        File file = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(file, new File("./sccreenshot.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        driver.quit();
        System.out.println("TestDescription finished");
    }
}

TestWatcherクラスを使うことで、テスト実行前、前提条件不成立時、テスト成功時、テスト失敗時、テスト終了後のタイミングで実行したい処理を実装できます。
今回は、初めにブラウザを起動します。そして最後にスクリーンショットを撮ってブラウザを閉じています。

InquiryFormTest.java

今回テストをするにあたり、ページオブジェクトパターンを使いました。
seleniumはテキストボックスに値を入れて送信するだとか、ラベルの文字が合っているかなどのテストをするわけですが、それぞれの要素をとってこなければいけません。
そこでその定義をクラスに分けて定義するという感じです。
InquiryFormPage.java

//import省略
public class InquiryFormPage {
    @FindBy(xpath = "//label[text()='名前']/following-sibling::div/input")
    public WebElement nameTextBox;

    @FindBy(xpath = "//label[text()='メールアドレス']/following-sibling::div/input")
    public WebElement emailTextBox;

    @FindBy(xpath = "//label[text()='内容']/following-sibling::div/textarea")
    public WebElement contentTextarea;

    @FindBy(xpath = "//button[text()='送信']")
    public WebElement confirmButton;

    @FindBy(xpath = "//button[text()='送信']")
    public WebElement submitButton;

    @FindBy(xpath = "//*[@id=\"complete\"]/p[1]")
    public WebElement complateMessege;

}

@FindByでその要素を取得する条件を書きます。
xpathのほかにidで取得したり、classで取得したりできます。
内部的には使われるタイミングになったら取得してくるようです。

次にやっとテストを書くクラスが出てきます。
InquiryFormTest.java

//import省略
public class InquiryFormTest extends Application {
    private static String TABLE_NAME = "inquiry";
    private static String expectedDataXmlPath = "datasets/testdata/sample.xml";
    private InquiryFormPage inquiryFormPage;

    //先ほど定義したRuleを使うための宣言
    @Rule
    public TestDescription testDescription = new TestDescription();
    @Rule
    public ExternalRule externalRule = new ExternalRule(testDescription, iDatabaseTester, iDatabaseConnection);

    public InquiryFormTest() throws Exception {
    }

    @Before
    public void setUp() {
        inquiryFormPage = PageFactory.initElements(testDescription.driver, InquiryFormPage.class);
        System.out.println("InquiryForm setup");
    }

    @Test
    public void inquiryFormTest() throws Exception {
        System.out.println("test1");
        testDescription.driver.get("http://localhost:8080/form");

        inquiryFormPage.nameTextBox.sendKeys("name");
        inquiryFormPage.emailTextBox.sendKeys("sample@sample.com");
        inquiryFormPage.contentTextarea.sendKeys("aaaaaaaaaaaa");
        inquiryFormPage.confirmButton.click();
        inquiryFormPage.submitButton.click();

        Assert.assertEquals("お問い合わせを受理しました。", inquiryFormPage.complateMessege.getText());

        IDataSet expected = new XmlDataSet(getClass().getResourceAsStream(expectedDataXmlPath));
        ITable expectedTable = expected.getTable(TABLE_NAME);
        IDataSet actual = iDatabaseTester.getConnection().createDataSet();
        ITable actualTable = actual.getTable(TABLE_NAME);

        Assertion.assertEquals(expected, actual);
        Assertion.assertEquals(expectedTable, actualTable);
    }
}

@beforeのsetUp()で使うページオブジェクトの定義をします。
これはテストごとに実行されます。
@Testを使ってテストを書いていきます。
流れとしては、

  • URLにアクセスし
  • フォームに値をいれ
  • 送信を押し
  • 問い合わせ受理の画面が出ているか
  • データベースに期待される値が入っているか

のテストをしています。
最後の期待されるデータを定義しているのがresources/datasets/testdata/sample.xmlになります。
sample.xml

<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="inquiry">
        <column>id</column>
        <column>name</column>
        <column>email</column>
        <column>content</column>
        <row>
            <value>1</value>
            <value>name</value>
            <value>sample@sample.com</value>
            <value>aaaaaaaaaaaa</value>
        </row>
    </table>
</dataset>

実行

前回作ったものを起動しつつ、今回作ったテストを実行してみてください。
テストが成功すればバーが緑色になると思います。
f:id:ZYPRESSEN:20170506104532p:plain

さいごに

githubのっけておきます。
github.com
次回は今回までに作ったものをJenkinsと連携させてみようと思います。