WordPressの自動テストを書く

最近フリーランスで契約しているお客さん先で幾つかのWordPressサイトの構築をお手伝いしています。WordPressは初めてだったんですが書籍「WordPressプラグイン開発のバイブル」がWP-CLIVCCWを利用したモダンな開発環境やテスト環境の構築方法について紹介していてとてもためになりました。

おかげでとても自動化が捗ってます^^ちなみに今回は開発と本番でChefのレシピを揃えたかったのでVCCWは使ってないんですが、VCCWはChefやServerspecが使われてますし、インストールされている構成も識者の方々のノウハウが詰まっているので、最初に目を通しておくと良いですね。レシピの書き方もためになります。

ただ、こちらの本でもテストの具体的な書き方については参考URLを紹介するに留める程度になっています。紹介されているのは「Automated Testing in WordPress, Really?!」というスライド。PHPUnitやSeleniumを利用したテストについて触れられています。

自分でググッて参考になったのはToro_Unitさんの下記のスライド。実体験ベースで日本語で読めるの分かりやすくて良いですね。

テストを最初に書くにあたってToro_UnitさんのCustom Post Type Permalinksテストを参考にさせて頂きました。人のテストを読むのが1番参考になる気がします。

で、最初に書いたのがこんな感じのテストです。作ってたのはインポーターで、ここではカテゴリーの登録をテストしています。

/**
 * 既存のカテゴリーを上書きできるか?
 */
function test_insert_or_update_term_should_update_existing_category() {
    $term_id = $this->importer->insert_or_update_term( 'cat1', 'cat1-slug', 'category', 0 );
    $term = get_term_by( 'term_id', $term_id, 'category' );
    $updated_term_id = $this->importer->insert_or_update_term( 'cat2', 'cat1-slug', 'category', 0 );
    $updated_term = get_term_by( 'term_id', $updated_term_id, 'category' );
    $this->assertEquals( $term_id, $updated_term_id );
    $this->assertNotEquals( $term->name, $updated_term->name );
    $this->assertEquals( $term->slug, $updated_term->slug );
}
/**
 * 複数のカテゴリーを登録できるか?
 */
function test_update_categories_should_register_categories() {
    $categories = [ new MockObj( 'cat1', 'cat1-slug' ) , new MockObj( 'cat2', 'cat2-slug' ) ];
    $this->importer->update_categories($categories, 'category', [
        'name' => 'parent1',
        'slug' => 'parent1-slug',
    ]);
    foreach ( $categories as $category ) {
        $term = get_term_by( 'slug', $category->code, 'category' );
        $this->assertFalse( is_wp_error( $term ) );
        $this->assertFalse( empty( $term ) );
        $this->assertEquals( $term->name, $category->name );
    }
}

単体テストとして範囲をしぼってテストすると必要とされるWordPressの構造やAPIの知識もテスト対象の機能に関連するものに留まるので、WordPress初心者でも十分書けるなと思いました。テストコードは引き継ぎ上お客さんにとっても役立ちますし、自分のコードの見直しにもなるので良いです。これからもどんどん書いていきたいです。

ちなみに最初にテストを書くにあたっていくつか疑問があったので、最後にそれらについてはどうだったかを備忘録的に書いておきたいと思います。

プラグインの設定等についてはどう記述するのか?

普通にupdate_optionを事前に呼び出しておけば反映されます。要するにWordPressの中で使える関数はそのまま使えてそれがDBにも反映されてくれるという事なんですね。簡単!

update_option( 'website', 'blog-a' );
update_option( 'db-hostname', 'localhost' );
update_option( 'db-port', '3306' );
update_option( 'db-dbname', 'another-db' );
update_option( 'db-username', 'user' );
update_option( 'db-password', 'password' );

依存するプラグインがある場合はどうするか?

これはどうするのが正しいのかまだ分かってません。本当はWordPressのユニットテストの機能でプラグインまでインストールできると良いんですが。ひとつの方法としてこちらの記事では自前でインストールスクリプトを書いています。記事のリンク先にGitHubがあるので、そちらで概要がつかめると思います。

具体的なスクリプトはこんな感じです。

install-dependencies.php

// https://github.com/misterbisson/bstat/blob/master/bin/install-dependencies.php
function download_plugin_via_git( $path, $plugin ) {
	// example: git clone https://github.com/GigaOM/go-ui.git $WP_CORE_DIR/wp-content/plugins/go-ui
	echo passthru( "git clone {$plugin['repo']} $path" ) . "\n\n";
	// update submodules, if any
	echo passthru( "cd $path; git submodule update --init --recursive" ) . "\n\n";
	return true;
}
function download_plugin( $path, $plugin ) {
	switch ( $plugin['repotype'] ) {
		case 'git':
			return download_plugin_via_git( $path, $plugin );
			return true;
		default:
			echo "Unsupported repo type {$plugin['repotype']}\n";
			return false;
	}
}
function download_plugins() {
	// this path needs to be kept in sync with the path set in install-wp-tests.sh
	// trailing slash expected
	$plugins_dir = '/tmp/wordpress/wp-content/plugins/';
	// the plugins to download
	$dependencies = require dirname( __DIR__ ). '/tests/dependencies-array.php';
	foreach ( $dependencies as $k => $dependency ) {
		if ( ! is_dir( $plugins_dir . $k ) ) {
			if ( download_plugin( $plugins_dir . $k, $dependency ) ) {
				echo "Downloaded $k\n";
			} else {
				echo "FAILED to download $k\n";
			}
		} else {
			echo "DIRECTORY EXISTS, skipped $plugins_dir$k\n";
		}
	}
}
download_plugins();

このコードを流用して、ぼくのプログラムのほうでは、Advanced Cutom Fieldsのフィールド定義のエクスポートファイルをWordPressインポートツールを使ってインポートする、という処理を入れてみました。

bootstrap.php

// WP_Importを読み込むために必要なフラグ
define( 'WP_LOAD_IMPORTERS', true );

$_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
	$_tests_dir = '/tmp/wordpress-tests-lib';
}

require_once $_tests_dir . '/includes/functions.php';

function _manually_load_plugin() {
	require dirname( dirname( __FILE__ ) ) . '/index.php';

	$local_plugin_directory = dirname( dirname( dirname( __FILE__ ) ) );

	$dependencies = require __DIR__ . '/dependencies-array.php';
	foreach ( $dependencies as $k => $dependency ) {
		// first try to get the plugin from the "real" WP install
		if ( is_dir( $local_plugin_directory . $k ) ) {
			require $local_plugin_directory . $dependency['include'];
			echo "Loaded $k\n";
			echo $local_plugin_directory . $dependency['include'];
		} elseif ( is_dir( WP_PLUGIN_DIR .'/' . $k ) ) {
			// try again, but in the "test" install (this is mostly for Travis)
			require WP_PLUGIN_DIR .'/' . $dependency['include'];
			echo "Loaded $k\n";
		} else {
			// give up
			echo "COULD NOT LOAD $k\n";
		}
	}
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );

require $_tests_dir . '/includes/bootstrap.php';

dependencies-array.php

return array(
	'advanced-custom-fields' => [
		'include' => 'advanced-custom-fields/acf.php',
		'repo' => 'https://github.com/elliotcondon/acf.git',
		'repotype' => 'git',
	],
	'wordpress-importer' => [
		'include' => 'wordpress-importer/wordpress-importer.php',
		'repo' => 'https://github.com/crowdfavorite-mirrors/wp-wordpress-importer.git',
		'repotype' => 'git',
	],
);

test_some_funcs_which_requires_acf_import.php

function setUp() {
    parent::setUp();
    ...
    $this->import_exported_acf_fields();
    ...
}

function import_exported_acf_fields() {
    $wp_importer = new WP_Import();
    $exported_file_path = dirname( dirname( __FILE__ ) ).'/resources/advanced-custom-field-export.xml';
    $wp_importer->import( $exported_file_path );
}

ちなみに今回自動テストを入れるにあたって早朝に起きて調べつつ書いてました。自動テストを入れる朝活けっこう捗って良いです。