スタッフブログ Staff Blog

CasperJS を用いたコンテンツ内容の自動チェック

CMS にコンテンツが正しく移行できているかどうかを確認する作業が発生しました。そこでWebkit ベースのヘッドレスなブラウザである PhantomJS を操作したりテストをサポートしたりするユーティリティ、 CasperJS を利用して自動でチェックを行うことを考えてみました。

SSL 使用でログインが必要だったのですが、意図したチェックが上手く行えましたので、少しご紹介したいと思います。

サイトへのログイン

まずサイトへのログインをどうするかですが、これは Casper モジュールの fillSelectors() を用いると簡単に実現できます。

試しに、 Facebook にログインして、自分のタイムラインに投稿をしてみました。
CasperJSでスクリプティングを行い、自分のタイムラインに投稿した結果

var casper = require("casper").create();

casper.start("https://www.facebook.com/", function () {
    this.fillSelectors("form#login_form", {
        "input[name='email']": "[Facebookログイン用のメールアドレス]",
        "input[name='pass']": "[Facebookログイン用パスワード]"
    }, true);
});

casper.thenEvaluate(function () {
    document.querySelector("#contentArea form #mentionsInput textarea").textContent = "CasperJSを使って、プログラムにログインフォームを入力させてログインをし、近況アップデートをするテスト。\n大量ページのテストに使えないかなーと。\nSSL対応は、`casperjs --ssl-protocol=tlsv1 [script_name]`で。";
    document.querySelector("#pagelet_composer div div div form").submit();
});

casper.run();

SSL なので --ssl-protocol=tlsv1 オプションを指定して実行しました。

$ casperjs --ssl-protocol=tlsv1 fbpost.js

指定の URL を読み込む

テストするページは新ページと旧ページが対になって大量にありました。そこで、自作の JSON を定義して読み込ませることにしました。 JSON は次のような形式にしました。( URL はスクリプトテスト用に使用したサンプルです。)

[
    {
        "id": 1,
        "oldURL": "http://www.itmedia.co.jp/news/articles/1509/03/news138.html",
        "newURL": "http://headlines.yahoo.co.jp/hl?a=20150903-00000081-zdn_n-game"
    },
    {
        "id": 2,
        "oldURL": "http://headlines.yahoo.co.jp/hl?a=20150903-35069877-cnetj-sci",
        "newURL": "http://japan.cnet.com/news/service/35069877/"
    }
]

次に上記の JSON を 利用してテストを実行するようにスクリプトを書きます。 JSON をパースして、1記事毎にループしてテストを実行するようにしています。テストは CasperJS の tester モジュールが利用できます。

指定の URL を開き、 .fetchText() で指定したセレクタのテキストを取得します。そして、 test.assertEquals() で値が同一かを判定します。

var fs = require("fs");
var json = fs.read("./urls.json");
var jsondata = JSON.parse(json);

jsondata.forEach(function (item, index) {
    var oldURL = item["oldURL"],
        newURL = item["newURL"];

    casper.test.begin("Entry ID:" + item["id"] + " Test", function (test) {
        var oldTitle,
            newTitle;

        casper.start(oldURL, function () {
            oldTitle = this.fetchText("h1");
        });

        casper.thenOpen(newURL, function () {
            newTitle = this.fetchText("h1");
        });

        casper.then(function () {
            test.assertEquals(oldTitle, newTitle);
        });

        casper.run(function () {
            test.done();
        });
    });
});

次のコードでテストを実行します。

$ casperjs test test.js

すると、次のように結果が表示され、テストをパスしたことが分かります。
2つの URL のタイトルが同一かをテストした画面

チェック本番

上記をふまえた上で、本番用のスクリプトを書きました。ログインの所は .fillSelectors() だと上手くいかない場合があるので、その時は後に掲載するサンプルのように document.querySelector() を使って地味に入力して送信します。

テストの際に1度だけログイン操作をするにはどうすれば良いか悩んだのですが、 casper.test.setUp() を利用し、なおかつ1回目のループの時だけログイン操作を実行するようにしました。

コンテンツは、先に紹介した .fetchText() に加え、 .getHTML() 等を利用して取得します。ただ、マークアップされていない場合もあるため、 try...catch を使用しました。

var fs = require("fs");
var json = fs.read("./urls.json");
var jsondata = JSON.parse(json);
var counter;

// 改行コード等の操作
function strAdjustment(str) {
    str = str.replace(/
/g, "");
    str = str.replace(/\r/g, "");
    str = str.replace(/\n/g, "");
    return str.trim();
}

// 旧記事から日付を取り出し、新記事の日付形式に変換する
function getOldEntryDate(str) {
    var re = /(\d+)年(\d+)月(\d+)日/;
    var found = str.match(re);
    var year = found[1];
    var month = found[2].length === 1 ? "0" + found[2] : found[2];
    var day = found[3].length === 1 ? "0" + found[3] : found[3];
    return year + "." + month + "." + day;
}

// 新記事から日付を取り出す
function getNewEntryDate(str) {
    var re = /\d{4}.\d{2}.\d{2}/;
    var found = str.match(re);
    return found[0];
}

casper.test.setUp(function (done) {
    if (counter > 0) {
        casper.start();
        casper.wait(250, function () {
            console.log("ログインスキップ");
        });
        casper.run(done);
        return;
    }

    casper.start();
    casper.userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36");

    // 新サイトログイン
    casper.thenOpenAndEvaluate("https://test1.com/admin/login", function () {
        document.querySelector("input[name='email']").setAttribute("value", "test@test1.com");
        document.querySelector("input[name='password']").setAttribute("value", "test1234");
        document.querySelector(".loginform").submit();
    });

    casper.then(function () {
        console.log("新サイトログイン状況:" + (this.getTitle() === "[確認用テキスト]" ? "OK" : "NG"));
    });

    // 旧サイトログイン
    casper.thenOpenAndEvaluate("https://test1.com/mt/admin", function () {
        document.querySelector("input[name='username']").setAttribute("value", "test@test2.com");
        document.querySelector("input[name='password']").setAttribute("value", "test5678");
        document.querySelector("form").submit();
    });

    casper.then(function () {
        console.log("旧サイトログイン状況:" + (this.getTitle() === "[確認用テキスト]" ? "OK" : "NG"));
    });

    casper.run(done);
});

jsondata.forEach(function (item, index) {
    counter = index;

    casper.test.begin("Entry ID:" + item["id"] + " Test", function (test) {
        var oldH1,
            oldH2,
            oldContent,
            oldDate,
            newH1,
            newH2,
            newContent,
            newDate;

        casper.thenOpen(item["oldURL"], function () {
            oldH1 = this.getHTML("article h1");
            try {
                oldH2 = this.getHTML("article h2");
            } catch (e) {
                oldH2 = "";
            }
            oldConetnt = strAdjustment(this.fetchText("article .contents"));
            oldDate = getOldEntryDate(this.getHTML("article .date"));
        });

        casper.thenOpen(item["newURL"], function () {
            newH1 = this.getHTML(".entry-header h1");
            try {
                newH2 = this.getHTML(".entry-header h2");
            } catch (e) {
                newH2 = "";
            }
            newConetnt = strAdjustment(this.fetchText(".entry-content"));
            newDate = getNewEntryDate(this.getHTML(".entry-meta"));
        });

        casper.then(function () {
            test.assertEquals(oldH1, newH1, "記事タイトルのテスト");
            test.assertEquals(oldH2, newH2, "副タイトルのテスト");
            test.assertEquals(oldConetnt, newConetnt, "コンテンツのテスト");
            test.assertEquals(oldDate, newDate, "リリース日のテスト");
        });

        casper.run(function () {
            test.done();
        });
    });
});

次のコマンドでテストを実行します。ちなみに、自己証明書を使用していたので、追加で --ignore-ssl-errors=true オプションの指定が必要でした。(ハマりました。。)

casperjs test --ssl-protocol=tlsv1 --ignore-ssl-errors=true sample.js

テスト結果は次のように表示されます。
テスト結果の表示サンプル。ログイン状況とコンテンツのテスト結果が表示されている。

おわりに

テストを書きながら、「機械的なチェックで果たして大丈夫なのか」と迷っていたのですが、 CMS への記事の移行ロジックをほぼ知らなかったこともあり、独自にテストコードを書いてそれで合否を決めても良いかな、と考えました。いくつかの記事を手でチェックしたのですが、結果問題ありませんでした。

また、人力でやるには膨大な時間とチェック方法の習得が必要になるなど、さまざまなハードルがあります。その点、プログラムで機械的にチェックを実行すれば、時間は最小限に、人力で続けていると起こりうるミスは防止できるのかなとも考えています。

当社では、より良い手法を常に模索して、よりよいサイトをご提供できるよう努めていきたいと考えております。

2015年9月8日追記

アクセス先によっては上手く動作しない場合もあるようです。その時は、 PhantomJS ではなく、 SlimerJS に変えてみると意図通り動作するかもしれません。 SlimerJS は真のヘッドレスブラウザでは無いのですが...、そこは今回のようなテストにはあまり関係無いので良しとします。

casperjs test --engine=slimerjs sample.js

お問い合わせ Contact

制作のご依頼、ご相談などは下記のフォームからご連絡ください。