Posted on

PHPのモッキンフレームワークPhakeの使い方

前回に引き続きPHPUnit関連の記事です。今回はPHPのモッキンフレームワークであるPhakeの使い方を確認します。
20161019203514

Phakeについて

作者は?

Mike Livelyという方のようです。

Phakeって?

こちらですね。

ファイル構成

一応ファイル構成を載せておきます。

.
├── Phake
│   ├── Annotation
│   │   ├── MockInitializer.php
│   │   └── Reader.php
│   ├── CallRecorder
│   │   ├── Call.php
│   │   ├── CallExpectation.php
│   │   ├── CallInfo.php
│   │   ├── IVerificationFailureHandler.php
│   │   ├── IVerifierMode.php
│   │   ├── OrderVerifier.php
│   │   ├── Position.php
│   │   ├── Recorder.php
│   │   ├── Verifier.php
│   │   ├── VerifierMode
│   │   │   ├── AtLeast.php
│   │   │   ├── AtMost.php
│   │   │   ├── Result.php
│   │   │   └── Times.php
│   │   └── VerifierResult.php
│   ├── ClassGenerator
│   │   ├── EvalLoader.php
│   │   ├── FileLoader.php
│   │   ├── ILoader.php
│   │   ├── InvocationHandler
│   │   │   ├── CallRecorder.php
│   │   │   ├── Composite.php
│   │   │   ├── FrozenObjectCheck.php
│   │   │   ├── IInvocationHandler.php
│   │   │   ├── MagicCallRecorder.php
│   │   │   └── StubCaller.php
│   │   └── MockClass.php
│   ├── Client
│   │   ├── Default.php
│   │   ├── IClient.php
│   │   └── PHPUnit.php
│   ├── Exception
│   │   ├── MethodMatcherException.php
│   │   └── VerificationException.php
│   ├── Facade.php
│   ├── IMock.php
│   ├── Matchers
│   │   ├── AbstractChainableArgumentMatcher.php
│   │   ├── AnyParameters.php
│   │   ├── ArgumentCaptor.php
│   │   ├── ChainedArgumentMatcher.php
│   │   ├── EqualsMatcher.php
│   │   ├── Factory.php
│   │   ├── HamcrestMatcherAdapter.php
│   │   ├── IArgumentMatcher.php
│   │   ├── IChainableArgumentMatcher.php
│   │   ├── IMethodMatcher.php
│   │   ├── IgnoreRemainingMatcher.php
│   │   ├── MethodMatcher.php
│   │   ├── PHPUnitConstraintAdapter.php
│   │   ├── ReferenceSetter.php
│   │   └── SingleArgumentMatcher.php
│   ├── Mock
│   │   ├── Freezer.php
│   │   ├── Info.php
│   │   └── InfoRegistry.php
│   ├── PHPUnit
│   │   ├── VerifierResultConstraint.php
│   │   └── VerifierResultConstraintV3d6.php
│   ├── Proxies
│   │   ├── AnswerBinderProxy.php
│   │   ├── AnswerCollectionProxy.php
│   │   ├── CallStubberProxy.php
│   │   ├── CallVerifierProxy.php
│   │   ├── StaticVisibilityProxy.php
│   │   ├── StubberProxy.php
│   │   ├── VerifierProxy.php
│   │   └── VisibilityProxy.php
│   ├── String
│   │   └── Converter.php
│   └── Stubber
│       ├── AnswerBinder.php
│       ├── AnswerCollection.php
│       ├── Answers
│       │   ├── ExceptionAnswer.php
│       │   ├── InvalidAnswerException.php
│       │   ├── LambdaAnswer.php
│       │   ├── NoAnswer.php
│       │   ├── ParentDelegate.php
│       │   ├── ParentDelegateCallback.php
│       │   ├── SelfAnswer.php
│       │   ├── SmartDefaultAnswer.php
│       │   └── StaticAnswer.php
│       ├── IAnswer.php
│       ├── IAnswerBinder.php
│       ├── IAnswerContainer.php
│       ├── SelfBindingAnswerBinder.php
│       └── StubMapper.php
└── Phake.php

PhakeはPhake.phpがファサードになっていてstaticに呼び出すのが特徴です。

Phakeの使い方

Phakeの使い方を前回同様以下の例を使って確認します。

例の補足

モック化の確認をしやすいように以下のパターンを用意しています。

  • 自分の中だけで完結する処理をするメソッド
  • クラス内のgetterを呼び出して処理しているメソッド
  • クラス内のsetterを呼び出して処理しているメソッド
  • privateメソッド
  • staticメソッド
class Example {
    private $total = 0;
    private static $static_total = 1;
    public function __construct() {
        $this->total = 1;
        self::$static_total = 2;
    }
    public function plusA() {
        return $this->total += 1;
    }
    public function plusB() {
        $this->total = $this->getTotal();
        return $this->total += 1;
    }
    public function plusC() {
        $this->setTotal(10);
        return $this->total += 1;
    }
    private function plusD() {
        return $this->total += 1;
    }
    public function getTotal() {
        return $this->total;
    }
    public function setTotal($total) {
        $this->total = $total;
    }
    public static function getStaticTotal() {
        return self::$static_total;
    }
}

Phake::mockで全モック

コンストラクタは呼ばれず全てのメソッドは握りつぶされます。その状態でメソッドを呼び出しても結果は取得できません。

/**
 * 全部握りつぶし
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock() {
    $example = Phake::mock(Example::class);
    $result = $example->plusA();
    $this->assertNull($result);
    $result = $example->plusB();
    $this->assertNull($result);
    $result = $example->plusC();
    $this->assertNull($result);
}
イメージ

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-20-42-52

Phake::mockで振る舞い設定

コンストラクタは呼ばれず全てのメソッドは握りつぶし、plusAにだけ返り値を設定します。

/**
 * 全部握りつぶし
 * plusAだけ振る舞い設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock_setting() {
    $example = Phake::mock(Example::class);
    Phake::when($example)->plusA()->thenReturn('A');
    $result = $example->plusA();
    $this->assertEquals('A', $result);
}
イメージ

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-20-59-27

Phake::partialMockで部分モック

partialMockだけを使った場合

まずpartialMockだけを使った場合、全ての振る舞いは元のままとなります。後述しますがこの場合verifyが使えます。

/**
 * 全部そのまま
 *
 * 引数:クラス(コンストラクタに値を渡す場合は引数追加)
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_non() {
    $example = Phake::partialMock(Example::class);
    $result = $example->plusA();
    $this->assertEquals(2, $result);
    $result = $example->plusB();
    $this->assertEquals(3, $result);
    $result = $example->plusC();
    $this->assertEquals(11, $result);
}
イメージ

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-20-19-38-07

partialMockで振る舞いを設定する場合

partialMockを使った後に以下のように振る舞いを設定します。PHPUnitのモックオブジェクトのcreateMockメソッドとは違いコンストラクタは呼ばれます。

/**
 * 全部そのまま
 * 指定したメソッドのみ振る舞い設定
 *
 * 引数:クラス(コンストラクタに値を渡す場合は引数追加)
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_partial() {
    $example = Phake::partialMock(Example::class);
    Phake::when($example)->getTotal()->thenReturn(3);
    $result = $example->plusA();
    $this->assertEquals(2, $result);
    $result = $example->plusB();
    $this->assertEquals(4, $result);
    $result = $example->plusC();
    $this->assertEquals(11, $result);
}
イメージ

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-20-19-45-49

どのように呼ばれたかを確認する

Phakeの場合どのように呼ばれたかのチェックは通常処理実行後にverifyを使って行います。getterやsetterがどの引数で何回呼ばれたかを確認するには以下のように書きます。

/**
 * 全部そのまま
 * 期待動作確認(1 test, 5 assertions)
 *
 * 引数:クラス(コンストラクタに値を渡す場合は引数追加)
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_setStubMethod() {
    $example = Phake::partialMock(Example::class);
    $result = $example->plusA();
    $this->assertEquals(2, $result);
    $result = $example->plusB();
    $this->assertEquals(3, $result);
    $result = $example->plusC();
    $this->assertEquals(11, $result);
    Phake::verify($example, Phake::times(1))->getTotal();
    Phake::verify($example, Phake::times(1))->setTotal(10);
}

privateメソッドをモック化する

ここまではPHPUnitのモックオブジェクトとそれほど変わりませんが、Phakeを使えばprivateメソッドを簡単に実行したりモック化することができます。

Phake::makeVisibleを使って普通に実行する

Phake::makeVisibleを使うことでprivateメソッドを実行することができます。

/**
 * 全部握りつぶさない
 * privateメソッドを普通に実行
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_private() {
    $example = Phake::partialMock(Example::class);
    // 直接は呼び出せないのでmakeVisibleにてプロキシして実行確認可能
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals(2, $result);
}

モック化する

以下のように普通にprivateなスタブメソッドに振る舞いを設定することもできます。

/**
 * 全部握りつぶさない
 * privateメソッドの振る舞いを設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれる
 */
public function test_partialMock_private2() {
    $example = Phake::partialMock(Example::class);
    // 普通に振る舞い設定
    Phake::when($example)->plusD()->thenReturn(5);
    // 直接は呼び出せないのでmakeVisibleにてプロキシして実行確認可能
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals(5, $result);
}

whenStaticを使ってstaticメソッドをモック化する

PhakeならstaticメソッドもPhake::whenStaticを使って簡単にモック化できてしまいます。

/**
 * 全部握りつぶし
 * staticメソッドの振る舞いを設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_partialMock_static() {
    // そのまま実行する場合
    $result = Example::getStaticTotal();
    $this->assertEquals(1, $result);
    $example = Phake::partialMock(Example::class);
    // 直接はNG
    $result = $example::getStaticTotal();
    $this->assertNull($result);
    // 振る舞い設定
    Phake::whenStatic($example)->getStaticTotal()->thenReturn(3);
    $result = $example::getStaticTotal();
    $this->assertEquals(3, $result);
}

その他に便利なもの

thenCallParent

以下のようにPhake::mockで全部握りつぶしたとしてもthenCallParentを使えば元の処理を呼び出すことができます。

/**
 * 全部握りつぶし
 * privateメソッドをスタブ化する
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock_private2() {
    $example = Phake::mock(Example::class);
    Phake::when($example)->plusD()->thenCallParent();
    // 普通にスタブ化
    Phake::when($example)->plusD()->thenReturn(5);
    // 直接は呼び出せないのでmakeVisibleにてプロキシして実行確認可能
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals(5, $result);
}

一括で振る舞いを設定する

モックに個別に振る舞いを設定するのが面倒なときは、Phake::ifUnstubbedを使って一括で設定できてしまいます。

/**
 * 全部スタブ化
 * 振る舞いを一括設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_mock_setting2() {
    $example = Phake::mock(Example::class, Phake::ifUnstubbed()->thenReturn('A'));
    $result = $example->plusA();
    $this->assertEquals('A', $result);
    $result = $example->plusB();
    $this->assertEquals('A', $result);
    $result = $example->plusC();
    $this->assertEquals('A', $result);
    $result = Phake::makeVisible($example)->plusD();
    $this->assertEquals('A', $result);
}

カバレッジレポート

最後にカバレッジレポートを見てみます。ソースはこちらにあります。
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-20-20-35-31
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-20-20-35-05
きました、オールグリーン!もちろんモック確認の為のクラスなんでオールグリーンになったところでたいした意味はありませんが、Phakeを使えば簡単にprivateもstaticもモック化できてとにかく楽ちんです。

まとめ

ここまで簡単にモック化できてしまうとPHPのモッキンフレームワークはPhakeで十分ですね。また、期待動作の設定を処理実行後のverifyで行うのも直感的でわかりやすいです。

今回使ったPHPUnitとPhakeのバージョン
  • “phpunit/phpunit”: “5.6.*”,
  • “phake/phake”: “2.3.*”,
Posted on

PHPUnitのモックオブジェクトの使い方を仕組みから理解する

前回はPHPUnitのメイン処理を確認しました。今回はPHPUnitデフォルトのモックオブジェクトの仕組みを確認してみます。公式ドキュメントでは、第9章 テストダブルが該当箇所となります。
20161019203514

PHPUnitのモックオブジェクトについて

PHPUnitは以下のような構成ですが、その中の「phpunit-mock-objects」がPHPUnitデフォルトのモックライブラリとなります。

  • phpunit
  • php-code-coverage
  • php-file-iterator
  • php-text-template
  • php-timer
  • php-token-stream
  • phpunit
  • phpunit-mock-objects ← これ

構成

PHPUnitモックオブジェクトのファイル構成は以下の通りです。

├── Builder
│   ├── Identity.php
│   ├── InvocationMocker.php
│   ├── Match.php
│   ├── MethodNameMatch.php
│   ├── Namespace.php
│   ├── ParametersMatch.php
│   └── Stub.php
├── Exception
│   ├── BadMethodCallException.php
│   ├── Exception.php
│   └── RuntimeException.php
├── Generator
│   ├── deprecation.tpl.dist
│   ├── 省略...
├── Generator.php
├── Invocation
│   ├── Object.php
│   └── Static.php
├── Invocation.php
├── InvocationMocker.php
├── Invokable.php
├── Matcher
│   ├── AnyInvokedCount.php
│   ├── AnyParameters.php
│   ├── ConsecutiveParameters.php
│   ├── Invocation.php
│   ├── InvokedAtIndex.php
│   ├── InvokedAtLeastCount.php
│   ├── InvokedAtLeastOnce.php
│   ├── InvokedAtMostCount.php
│   ├── InvokedCount.php
│   ├── InvokedRecorder.php
│   ├── MethodName.php
│   ├── Parameters.php
│   └── StatelessInvocation.php
├── Matcher.php
├── MockBuilder.php
├── MockObject.php
├── Stub
│   ├── ConsecutiveCalls.php
│   ├── Exception.php
│   ├── MatcherCollection.php
│   ├── Return.php
│   ├── ReturnArgument.php
│   ├── ReturnCallback.php
│   ├── ReturnReference.php
│   ├── ReturnSelf.php
│   └── ReturnValueMap.php
├── Stub.php
└── Verifiable.php

今回は中でも主要処理のあるGenerator、MockBuilder、InvocationMockerをチェックしました。

スタブとモックオブジェクトについて

モックオブジェクトを確認する前に、スタブとモックオブジェクトについて簡単に整理しておきます。PHPUnitの公式ドキュメントでは以下のように記載しています。
スタブ

実際のオブジェクトを置き換えて、 設定した何らかの値を (オプションで) 返すようなテストダブルのことを スタブ といいます。

モックオブジェクト

実際のオブジェクトを置き換えて、 (メソッドがコールされたことなどの) 期待する内容を検証するテストダブルのことを モック といいます。

違いがちょっとわかりにくいですが、スタブもモックオブジェクトもテストの為の代用品(テストダブル)であることに変わりはありません。では何が違うのか。それは使われ方によって変わってきます。(※個人的な見解も含んでいます。)
テストをする際には、必要に応じてオブジェクトをモック化する必要があり、その際メソッドを握りつぶし何らかの振る舞いを設定するのですが、そのテストの代用品自体がスタブとなります。モックオブジェクトは、モック化したスタブオブジェクトをテスト対象クラスに差し込み、差し込んだモックオブジェクトが想定通り使われているかを確認する為に使うことでモックオブジェクトとなります。
言葉だけだとイメージしにくいので図にしてみました。
スタブ
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-14-10-59-32
モックオブジェクト
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-12-18-37-26
モック化したオブジェクトをスタブと呼ぶかモックと呼ぶかはいまいちはっきりわからなかったので以下ではモックと統一して進めていきます。

モックの使い方

では具体的なモックの使い方を確認してみます。
20161024204138
PHPUnitのTestCaseクラスはモックの呼び出し処理をいくつかラップしているのでTestCaseを継承していればモックを利用することができます。
PHPUnitのモックの呼び出しは大きく以下の3つの方法があります。

  • 自分で1からプロパティを設定してモックを作成する「getMockBuilder」を使う方法
  • 予めプロパティを設定されたモックを取得する「createMock」を使う方法
  • モック化したいクラス名とプロパティ情報を引数で渡してモックを生成する「getMock」を使う方法(※現在非推奨)

その中でも普通に使う分には「createMock」を使う方法が一番楽で良いかと思います。ただこの場合コンストラクタが呼ばれないので、コンストラクタを呼びたい場合や他に設定を変えたい場合は「getMockBuilder」を使ってモック化を行います。

createMockの処理を簡単に確認する

createMock

  • getMockBuilderでモックを取得
  • 必要なプロパティをメソッドチェーンで設定する
  • MockBuilderはGeneratorを使って自身のプロパティを渡しモックを生成する
    ※ createMockを使う場合コンストラクタ含め例外を除く全メソッドが握りつぶされます。
protected function createMock($originalClassName)
{
    return $this->getMockBuilder($originalClassName)
                ->disableOriginalConstructor()
                ->disableOriginalClone()
                ->disableArgumentCloning()
                ->disallowMockingUnknownTypes()
                ->getMock();
}

createPartialMock

createPartialMockをいうメソッドも用意されているので見てみます。これは指定したメソッドだけを握りつぶすことができます。指定しない場合は全て元のまま呼び出せます。

protected function createPartialMock($originalClassName, array $methods)
{
    return $this->getMockBuilder($originalClassName)
                ->disableOriginalConstructor()
                ->disableOriginalClone()
                ->disableArgumentCloning()
                ->disallowMockingUnknownTypes()
                ->setMethods(empty($methods) ? null : $methods)
                ->getMock();
}

createConfiguredMock

createConfiguredMockも一応見てみると、これはcreateMock+メソッドの振る舞い設定の組み合わせのfunctionです。

protected function createConfiguredMock($originalClassName, array $configuration)
{
    $o = $this->createMock($originalClassName);
    foreach ($configuration as $method => $return) {
        $o->method($method)->willReturn($return);
    }
    return $o;
}

モック生成の具体例

実際にcreateMockとcreatePartialMockを使ったモック生成方法を以下の例を使って確認してみます。

テスト対象のクラス例

何でもない変なクラスですが一応補足しておくと確認しやすいように以下のメソッドを用意しています。

  • ただ処理するだけのメソッド
  • クラス内のgetterを呼び出して処理しているメソッド
  • クラス内のsetterを呼び出して処理しているメソッド
  • privateメソッド
  • staticメソッド
class Example {
    private $total = 0;
    private static $static_total = 1;
    public function __construct() {
        $this->total = 1;
        self::$static_total = 2;
    }
    public function plusA() {
        return $this->total += 1;
    }
    public function plusB() {
        $this->total = $this->getTotal();
        return $this->total += 1;
    }
    public function plusC() {
        $this->setTotal(10);
        return $this->total += 1;
    }
    private function plusD() {
        return $this->total += 1;
    }
    public function getTotal() {
        return $this->total;
    }
    public function setTotal($total) {
        $this->total = $total;
    }
    public static function getStaticTotal() {
        return self::$static_total;
    }
}

createMockで全モック

コンストラクタは呼ばれず全てのメソッドは握りつぶされます。その状態でメソッドを呼び出しても結果は取得できません。

/**
 * 全メソッド握りつぶす
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_createMock_all() {
    $this->example = $this->createMock(Example::class);
    $result = $this->example->plusA();
    $this->assertNull($result);
    $result = $this->example->plusB();
    $this->assertNull($result);
    $result = $this->example->plusC();
    $this->assertNull($result);
}

イメージ
全部呼び出せない。
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-20-42-52

createMockで振る舞い設定

/**
 * 全メソッド握りつぶす
 * plusAだけに振る舞い設定
 *
 * 引数:クラスのみ
 * コンストラクタ:呼ばれない
 */
public function test_createMock_all_stub() {
    $this->example = $this->createMock(Example::class);
    $this->example->expects($this->once())->method('plusA')->willReturn('A');
    $result = $this->example->plusA();
    $this->assertEquals('A', $result);
}

イメージ
plusAだけ呼び出せて(実態はよばれない)結果は自分の設定した値になる。
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-20-59-27

createPartialMockで部分モック

第一引数に対象のクラスと第二引数に握りつぶしたいメソッドを配列で指定すると部分的にモック化することができる。第二引数を指定しない場合(または空の配列を指定)すべてのメソッドは元のまま。

/**
 * 一部握りつぶす
 *
 * 引数:クラスとメソッド
 * コンストラクタ:呼ばれない
 */
public function test_createPartialMock_partial() {
    $this->example = $this->createPartialMock(Example::class, ['getTotal']);
    $result = $this->example->plusA();
    $this->assertEquals(1, $result);
    // getTotalは振る舞いを設定していないためnullとなるがエラーにはならない
    $result = $this->example->plusB();
    $this->assertEquals(1, $result);
    $result = $this->example->plusC();
    $this->assertEquals(11, $result);
}

イメージ
一部握りつぶし他は元のまま
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-21-09-15
振る舞いを指定する場合は以下のようにする

/**
 * 一部スタブ化&振る舞い設定
 *
 * 引数:クラスとメソッド
 * コンストラクタ:呼ばれない
 */
public function test_createPartialMock_setStub() {
    $this->example = $this->createPartialMock(Example::class, ['getTotal']);
    $this->example->expects($this->once())->method('getTotal')->willReturn(3);
    $result = $this->example->plusA();
    $this->assertEquals(1, $result);
    $result = $this->example->plusB();
    $this->assertEquals(4, $result);
    $result = $this->example->plusC();
    $this->assertEquals(11, $result);
}

イメージ
一部握りつぶし振る舞いを設定し他は元のまま
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-15-21-07-59
また、setterのようにどの引数で何回呼ばれたかを確認するには以下のように書きます。

/**
 * 一部スタブ化&期待値設定(1 test, 4 assertions)
 *
 * 引数:クラスとメソッド
 * コンストラクタ:呼ばれない
 */
public function test_createPartialMock_setStub2() {
    $this->example = $this->createPartialMock(Example::class, ['setTotal']);
    $this->example->expects($this->once())->method('setTotal')->with(10);
    $result = $this->example->plusA();
    $this->assertEquals(1, $result);
    $result = $this->example->plusA();
    $this->assertEquals(2, $result);
    // setTotalは期待値の設定はあるが振る舞いはないので実行はされない
    $result = $this->example->plusC();
    $this->assertEquals(3, $result);
}

モックの振る舞いを設定する処理について

最後にモックに振る舞いを設定する処理について簡単に触れておきます。モックの振る舞いを設定する処理はInvocationMockerクラスが担っていて、method、willReturn、expects、withなどを使って振る舞いや期待値を設定します。
20161024204136

method

振る舞いや期待値を設定するメソッドを指定する

willReturn

スタブメソッドにどのような値を返させるかを設定する

expects

スタブメソッドが何回呼ばれるかを設定する

with

スタブメソッドがどの引数を受け取るかを設定する

カバレッジレポート

これらのテストのカバレッジレポートを出力します。カバレッジレポートは以下のように出力することができます。サンプルソースはこちらにあります。
出力方法

phpunit --bootstrap Bootstrap.php --coverage-html ./report --whitelist src/ test/ExamplePhpUnitMockTest.php

カバレッジレポート
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-16-17-25-14
%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-12-16-17-25-34
コンストラクタとprivateとstaticとgetTotalメソッドはテストを通していないので赤く塗られます。また、privateとstaticメソッドのモック化やテストは面倒なので別のモッキンフレームワークであるPhakeなどでやったほうがいいと思います。
これで大体のモック仕組みとの基本の使い方の確認の確認ができました。これらを使って組み合わせるだけでも多くのテストの実行がカバーできるかと思います。

今回使ったPHPUnitのバージョン

“phpunit/phpunit”: “5.6.*”

Posted on

PHPUnitの使い方を仕組みから理解する

ここ数年仕事ではPHPを使って開発をしていますが、最近品質について考える機会が増えたこともあり、これを機にPHPUnitと周辺のモジュールの仕組みを理解してより楽にテストができるようにしたいと思います。
20161019203514

PHPUnitは?

Sebastian Bergmann

Created PHPUnit. Co-Founded thePHP.cc. Helps PHP developers build better software.

PHPUnitの作者は、Sebastian Bergmannという方でthePHP.ccのファウンダーのようです。関連情報は以下にて確認してみてください。

PHPUnitの構成

PHPUnitは以下のような構成になっています。

  • phpunit
    • php-code-coverage
    • php-file-iterator
    • php-text-template
    • php-timer
    • php-token-stream
    • phpunit
    • phpunit-mock-objects

これらはGitHub上ではそれぞれ別々のリポジトリに分かれていますが、phpunitが本体でそれ以外はデフォルトの関連ライブラリという位置づけになるかと思います。

PHPUnitのsrc構成

モックオブジェクトなどを除いたphpunitだけのパッケージとクラス構成を見てみるとこんな感じになります。

├── Exception.php
├── Extensions
│   ├── GroupTestSuite.php
│   ├── PhptTestCase.php
│   ├── PhptTestSuite.php
│   ├── RepeatedTest.php
│   ├── TestDecorator.php
│   └── TicketListener.php
├── ForwardCompatibility
│   └── TestCase.php
├── Framework
│   ├── Assert
│   │   └── Functions.php
│   ├── Assert.php
│   ├── AssertionFailedError.php
│   ├── BaseTestListener.php
│   ├── CodeCoverageException.php
│   ├── Constraint
│   │   ├── And.php
│   │   ├── ArrayHasKey.php
│   │   ├── ArraySubset.php
│   │   ├── Attribute.php
│   │   ├── Callback.php
│   │   ├── ClassHasAttribute.php
│   │   ├── ClassHasStaticAttribute.php
│   │   ├── Composite.php
│   │   ├── Count.php
│   │   ├── Exception.php
│   │   ├── ExceptionCode.php
│   │   ├── ExceptionMessage.php
│   │   ├── ExceptionMessageRegExp.php
│   │   ├── FileExists.php
│   │   ├── GreaterThan.php
│   │   ├── IsAnything.php
│   │   ├── IsEmpty.php
│   │   ├── IsEqual.php
│   │   ├── IsFalse.php
│   │   ├── IsFinite.php
│   │   ├── IsIdentical.php
│   │   ├── IsInfinite.php
│   │   ├── IsInstanceOf.php
│   │   ├── IsJson.php
│   │   ├── IsNan.php
│   │   ├── IsNull.php
│   │   ├── IsTrue.php
│   │   ├── IsType.php
│   │   ├── JsonMatches
│   │   │   └── ErrorMessageProvider.php
│   │   ├── JsonMatches.php
│   │   ├── LessThan.php
│   │   ├── Not.php
│   │   ├── ObjectHasAttribute.php
│   │   ├── Or.php
│   │   ├── PCREMatch.php
│   │   ├── SameSize.php
│   │   ├── StringContains.php
│   │   ├── StringEndsWith.php
│   │   ├── StringMatches.php
│   │   ├── StringStartsWith.php
│   │   ├── TraversableContains.php
│   │   ├── TraversableContainsOnly.php
│   │   └── Xor.php
│   ├── Constraint.php
│   ├── Error
│   │   ├── Deprecated.php
│   │   ├── Notice.php
│   │   └── Warning.php
│   ├── Error.php
│   ├── Exception.php
│   ├── ExceptionWrapper.php
│   ├── ExpectationFailedException.php
│   ├── IncompleteTest.php
│   ├── IncompleteTestCase.php
│   ├── IncompleteTestError.php
│   ├── InvalidCoversTargetException.php
│   ├── OutputError.php
│   ├── PHPUnit_Framework_CoveredCodeNotExecutedException.php
│   ├── PHPUnit_Framework_MissingCoversAnnotationException.php
│   ├── RiskyTest.php
│   ├── RiskyTestError.php
│   ├── SelfDescribing.php
│   ├── SkippedTest.php
│   ├── SkippedTestCase.php
│   ├── SkippedTestError.php
│   ├── SkippedTestSuiteError.php
│   ├── SyntheticError.php
│   ├── Test.php
│   ├── TestCase.php
│   ├── TestFailure.php
│   ├── TestListener.php
│   ├── TestResult.php
│   ├── TestSuite
│   │   └── DataProvider.php
│   ├── TestSuite.php
│   ├── UnintentionallyCoveredCodeError.php
│   ├── Warning.php
│   └── WarningTestCase.php
├── Runner
│   ├── BaseTestRunner.php
│   ├── Exception.php
│   ├── Filter
│   │   ├── Factory.php
│   │   ├── Group
│   │   │   ├── Exclude.php
│   │   │   └── Include.php
│   │   ├── Group.php
│   │   └── Test.php
│   ├── StandardTestSuiteLoader.php
│   ├── TestSuiteLoader.php
│   └── Version.php
├── TextUI
│   ├── Command.php
│   ├── ResultPrinter.php
│   └── TestRunner.php
└── Util
    ├── Blacklist.php
    ├── Configuration.php
    ├── ConfigurationGenerator.php
    ├── ErrorHandler.php
    ├── Fileloader.php
    ├── Filesystem.php
    ├── Filter.php
    ├── Getopt.php
    ├── GlobalState.php
    ├── InvalidArgumentHelper.php
    ├── Log
    │   ├── JSON.php
    │   ├── JUnit.php
    │   ├── TAP.php
    │   └── TeamCity.php
    ├── PHP
    │   ├── Default.php
    │   ├── Template
    │   │   └── TestCaseMethod.tpl.dist
    │   ├── Windows.php
    │   └── eval-stdin.php
    ├── PHP.php
    ├── Printer.php
    ├── Regex.php
    ├── String.php
    ├── Test.php
    ├── TestDox
    │   ├── NamePrettifier.php
    │   ├── ResultPrinter
    │   │   ├── HTML.php
    │   │   ├── Text.php
    │   │   └── XML.php
    │   └── ResultPrinter.php
    ├── TestSuiteIterator.php
    ├── Type.php
    └── XML.php

PHPUnitのメイン処理

PHPUnitでテストを実行するときのメイン処理をざくっと見てみます。
20161019200553
PHPUnit_TextUI_Command
PHPUnit_TextUI_TestRunner
TextUIのCommandとTestRunnerはphpunitのエントリポイントでphpunit実行時CommandからTestRunnerを呼び出し処理を開始します。
PHPUnit_Framework_Test(interface)
PHPUnit_Framework_TestSuite
TestRunnerはTestSuiteを作成しTestSuiteのメイン処理であるrun処理を実行します。
20161019195137
PHPUnit_Framework_Test(interface)
PHPUnit_Framework_Assert
PHPUnit_Framework_TestCase
PHPUnit_Framework_TestResult
20161019195138
TestSuiteはforeachにて順次TestCaseのテストを実行します。また、TestCaseの親クラスにはAssertがいてアサーションができるようになっています。

テストを実行する該当ソースをチェックする

TestSuite.php

TestRunnerから呼び出されたTestSuiteは、run処理にて対象テストをforeachして順次テストを実行します。

foreach ($this as $test) {
    if ($result->shouldStop()) {
        break;
    }
    if ($test instanceof PHPUnit_Framework_TestCase ||
        $test instanceof self) {
        $test->setbeStrictAboutChangesToGlobalState($this->beStrictAboutChangesToGlobalState);
        $test->setBackupGlobals($this->backupGlobals);
        $test->setBackupStaticAttributes($this->backupStaticAttributes);
        $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);
    }
    $test->run($result);
}

TestCase.php

TestCaseのrun処理ではTestResultのrun処理を実行します。

if ($result === null) {
    $result = $this->createResult();
}
  ...省略...
$result->run($this);

TestResult.php

TestResultのrun処理では、渡されたTestCaseのrunBare()を実行します。

$test->runBare();

TestCase.php

TestCaseのrunBare処理がテスト実行の最終地点で、before・after等のhookポイントを使いsetUp、テスト、tearDownを実行します。TestResultから呼び出しているのはテスト結果をTestResultが処理する為でしょう。実行後は順次TestResultが処理の呼び出し元に順に返され処理が終了します。

foreach ($hookMethods['before'] as $method) {
    $this->$method();
}
$this->assertPreConditions();
$this->testResult = $this->runTest();
$this->verifyMockObjects();
$this->assertPostConditions();

かなりざっくりではありますがphpunitのメイン処理を確認してみました。だいたいの処理の流れは分かったので次回はモックオブジェクトを見ていこうと思います。

Posted on

FlaskをBottleと比較した雑感

以前Bottleのソースをチェックしてみた流れでFlaskも見てみた。結論から言うとBottleと大して変わらん。もちろんFlaskのほうがコード量が多く多少リッチではあるもののざっくり機能ベースで言うと大して変わらんのです。
そもそも両方ともマイクロWebフレームワークが売りなので当たり前といえば当たり前ですが、歴史的にもBottleが2009年リリースでFlaskが2010年4月1日(エイプリルフールのネタ
として)リリースと、名前もBottleに対抗してFlask(フラスコ)という名前をつけたということなので、もともとがBottleっぽいフレームワークを遊びで作ってみたという感じなんだと思います。それっぽいことはwikiに書かれています。

  • [Bottle Wiki](https://en.wikipedia.org/wiki/Bottle_(web_framework)
  • Flask Wiki

クラス構成に関しては以下にある通りで、Bottleが1ファイル構成でFlaskは機能にファイルが分かれてる構成という違いはあるものの、機能自体にほとんど違いはありません。ただFlaskはBlueprintという大規模アプリ対応の機能があります。
20160628103203
なお、使い方については公式ドキュメントとサンプルコードも複数用意されているのでアプリの構成や使い方が確認しやすいです。

Posted on

Git 2.9でdiffがちょっとかしこくなった

ちょっと前にはなるがGit 2.9がリリースされてdiffがちょっとかしこくなった。

リリースノート

記事

diffをいままでよりいい感じにする

git diffに「–compaction-heuristic」オプションを追加するかgit configに「diff.compactionHeuristic true」を設定することで、これまでdiffがコンフリクト起こして正しく出せてなかったケースをカバーするようになった。今回のリリースではオプションつけるか設定変更する必要があるけど後々デフォルトになる予定とのこと。ちなみにほんとかな?と記事のソースで新旧バージョンで比較してみたところしっかり解消されてました。はいすいません。
オプションつける場合

git diff --compaction-heuristic

コンフィグに設定する場合

git config --global diff.compactionHeuristic true

ハイライトもこれまでよりいい感じにできる

diff-highlightでは同行の文字変更を見やすくするというものだけどこれ自体は以前からあった。ではどこが変わったかというと、これまでごく一部でハイライトできなかったけどそれ解消したよってことだと思う。でこっちも設定変える必要あるよとのこと。
設定がなければ追加

git config --global pager.log 'diff-highlight | less'
git config --global pager.show 'diff-highlight | less'
git config --global pager.diff 'diff-highlight | less'

今回のリリースで追加された新しい設定

git config interactive.diffFilter diff-highlight

ということで今回のリリースによって.gitconfigに設定が追加された。

[diff]
    compactionHeuristic = true
[interactive]
    diffFilter = diff-highlight
Posted on

Python Bottleのソースを読む ユニットテスト編

Python Bottleのソースを読む テスト編
前回までで一通りメインの機能は確認できました。ではいよいよコード追加してプルリクを投げましょうというところですが、テストは?ということでユニットテストとカバレッジ状況をチェックしてみます。また自作アプリに対してのテスト方法もチェックしてみます。

Bottleのユニットテストとカバレッジ状況

GitHub上のBottleをクローンすると直下にtestディレクトリがあります。ここにずらーっとテストがありますが、よくよく見てみるとtestall.pyという全実行のエントリポイントが用意されているのでそれを実行します。

# noseを入れてない場合はインストール
pip install nose
# こんな感じに実行
nosetests --with-coverage --cover-html testall.py

でました。
20160623211937
bottle.pyだけを見てみると
76%
しっかりテストが書かれてるのがわかります。

bottle.py                       2277    485    849     79    76%

そしてhtmlをチェック
20160623211742
コードのカバレッジ状況が一目瞭然。あとは足りないとこテストしたり自分で処理追加してテストしたり。これでいろいろ安心。

BottleでつくったWebアプリのテスト方法

せっかくなのでWebアプリのテスト方法も見てみます。これは公式ドキュメントにしっかり書かれているのでそこをみればだいたい分かると思います。
noseインストール

pip install nose

テスト対象コード

import bottle
@bottle.route('/')
def index():
    return 'Hi!'
if __name__ == '__main__':
    bottle.run()

テストコード
単純にindex()を実行して結果をassertする

import mywebapp
def test_webapp_index():
    assert mywebapp.index() == 'Hi!'

テスト実行

nosetests test_app.py
Ran 1 test in 0.014s
OK

上記は直接関数をテストする方法ですがwebtestを使ってwebベースでアクセスしてテストする方法もあります。ただこの方法をやってみたところ明示的 app = Bottle()を宣言しないとうまく実行できませんでした。ここら辺はもう少しwebtestの使い方がわかればなんとかなるかもしれません。
webtestインストール

pip install webtest

テスト対象コード

from bottle import Bottle
# アプリを明示的に作成
app = Bottle()
@app.route('/')
def index():
    return 'Hi!'
if __name__ == '__main__':
    run(app=app)

テストコード
公式ドキュメントではログイン・ログアウトのテストが書かれてましたがここでは省略

from webtest import TestApp
import mywebapp
def test_index():
    app = TestApp(mywebapp.app)
    assert app.get('/').status == '200 OK'

テスト実行

nosetests test_app.py
Ran 2 tests in 0.003s
OK

これでいろいろ安心

Posted on

Python Bottleのソースを読む ルータ編

Pythonの軽量WebフレームワークBottleのソースを読む ルータ編
 

Class Hierarchy

ここの部分
20160208112338
 

Bottleのルータについて

bottleのルータは特に継承関係はなくRouteとRouterクラスで構成されている

Routerの役割

A Router is an ordered collection of route->target pairs. It is used to efficiently match WSGI requests against a number of routes and return the first target that satisfies the request. The target may be anything, usually a string, ID or callable object. A route consists of a path-rule and a HTTP method.
The path-rule is either a static path (e.g. `/contact`) or a dynamic path that contains wildcards (e.g. `/wiki/`). The wildcard syntax and details on the matching order are described in docs:`routing`.

RouterはRoute情報のコレクションを保持する。

Routeの役割

This class wraps a route callback along with route specific metadata and configuration and applies Plugins on demand. It is also responsible for turing an URL path rule into a regular expression usable by the Router.

Routeはrouteに対するcallback関数やroute特有の情報を保持するオブジェクト。RouteオブジェクトをRouterが一覧で保持することになる。

ルータの使い方

ルータは以下のようにデコレータを指定する。@getや@postはそれぞれGETのみPOSTのみを受け付ける

@route('/routes')
def routes():
    return 'routes'
@get('/get')
def get():
    return 'get'
@post('/post')
def post():
    return 'post'

デコレータには以下が用意されている

route     = make_default_app_wrapper('route')
get       = make_default_app_wrapper('get')
post      = make_default_app_wrapper('post')
put       = make_default_app_wrapper('put')
delete    = make_default_app_wrapper('delete')
patch     = make_default_app_wrapper('patch')
error     = make_default_app_wrapper('error')
mount     = make_default_app_wrapper('mount')
hook      = make_default_app_wrapper('hook')
install   = make_default_app_wrapper('install')
uninstall = make_default_app_wrapper('uninstall')
url       = make_default_app_wrapper('get_url')

ルータの処理

まず起動時のBottleインスタンス化時(default_appが生成されAppStackにpushされる)に、コンストラクタにてRouterがセットされる。

app = default_app = AppStack()
app.push()

コントローラのデコレータを読み込む

@get('/get')
def get():
    return 'get'

make_default_app_wrapperが呼ばれBottleのdef getが呼ばれる

def make_default_app_wrapper(name):
    """ Return a callable that relays calls to the current default app. """
    @functools.wraps(getattr(Bottle, name))
    def wrapper(*a, **ka):
        return getattr(app(), name)(*a, **ka)
    return wrapper

def getなどはrouteを少しラップしたもの

def get(self, path=None, method='GET', **options):
    """ Equals :meth:`route`. """
    return self.route(path, method, **options)
def post(self, path=None, method='POST', **options):
    """ Equals :meth:`route` with a ``POST`` method parameter. """
    return self.route(path, method, **options)
def put(self, path=None, method='PUT', **options):
    """ Equals :meth:`route` with a ``PUT`` method parameter. """
    return self.route(path, method, **options)
def delete(self, path=None, method='DELETE', **options):
    """ Equals :meth:`route` with a ``DELETE`` method parameter. """
    return self.route(path, method, **options)
def patch(self, path=None, method='PATCH', **options):
    """ Equals :meth:`route` with a ``PATCH`` method parameter. """
    return self.route(path, method, **options)

routeのdecoratorでRouteをインスタンス化(loadでcallback functionを取得しruleとセットで渡す)しBottleのroutesとRouterにrouteを追加する。これらを繰り返しrouteをすべて読み込む。これでpathから特定のactionを呼び出すことができるようになる。

def decorator(callback):
    if isinstance(callback, basestring): callback = load(callback)
    for rule in makelist(path) or yieldroutes(callback):
        for verb in makelist(method):
            verb = verb.upper()
            route = Route(self, rule, verb, callback,
                          name=name,
                          plugins=plugins,
                          skiplist=skiplist, **config)
            self.add_route(route)
    return callback
Posted on

Python Bottleのソースを読む プラグイン編

Pythonの軽量WebフレームワークBottleのソースを読む プラグイン編
Bottleを触ってみると通常のWebフレームワークには用意されているであろう機能がなかったりします。これはマイクロフレームワークであるが故であり、すべてがそろってない状態がむしろ正しい状態と言えます。Bottleではそういったものを補うためにプラグインが用意されていてある程度の機能はそちらでまかなうことができます。また、Plugin Development Guide を参考にしてプラグインを自作することも可能です。

Class Hierarchy

plugin用クラスはなくインターフェースが定義されているのでそれにしたがって実装します。

プラグインの使い方

公式ドキュメントに簡単な使い方が乗っているのでこちらを参考にすれば簡単に導入することができます。
以下がサンプルコードで、簡単な流れとしては
install()で任意のプラグインをインストールする
リクエスト時にプラグイン実行
となります。サンプルコードでは、プラグインでkwargsにdbをセットされている為ルートのアクションでdb変数が利用できるようになってます。

from bottle import route, install, template
from bottle_sqlite import SQLitePlugin
install(SQLitePlugin(dbfile='/tmp/test.db'))
@route('/show/<post_id:int>')
def show(db, post_id):
    c = db.execute('SELECT title, content FROM posts WHERE id = ?', (post_id,))
    row = c.fetchone()
    return template('show_post', title=row['title'], text=row['content'])
@route('/contact')
def contact_page():
    ''' This callback does not need a db connection. Because the 'db'
        keyword argument is missing, the sqlite plugin ignores this callback
        completely. '''
    return template('contact')

プラグインの作り方

プラグインの作り方も公式ドキュメントにもっともシンプルな形のサンプルがあります。これは実行速度をレスポンスヘッダーにつけて返す処理ですが、サーバ起動時にstopwatchをインストールし、リクエストが来た際にデコレータを実行(表現あってるか分からない)することでリクエストの処理時間は計測できるようになっています。

from bottle import response, install, route
import time
def stopwatch(callback):
    def wrapper(*args, **kwargs):
        start = time.time()
        body = callback(*args, **kwargs)
        end = time.time()
        response.headers['X-Exec-Time'] = str(end - start)
        return body
    return wrapper
install(stopwatch)
@route('/')
def index():
    return 'INDEX'

プラグインのinstall()処理をチェック

プラグインはインターフェースが定義されているのでそれに従って書く必要がありますが、setup()で事前準備をして、apply()で実際にプラグインの処理を実行するといった流れになります。実際にはstopwatch()のように関数だけを実装して渡すことも可能です。

def install(self, plugin):
    """ Add a plugin to the list of plugins and prepare it for being
        applied to all routes of this application. A plugin may be a simple
        decorator or an object that implements the :class:`Plugin` API.
    """
    if hasattr(plugin, 'setup'): plugin.setup(self)
    if not callable(plugin) and not hasattr(plugin, 'apply'):
        raise TypeError("Plugins must be callable or implement .apply()")
    self.plugins.append(plugin)
    self.reset()
    return plugin

プラグイン組み合わせて使うことでスムーズにBottleを使ったWeb開発が進められるようになります。ここまでで主要機能は一通りみることができたので、次回はテストをチェックしてみます。

Posted on

Python Bottleのソースを読む リクエスト・レスポンス編

Pythonの軽量WebフレームワークBottleのソースを読む リクエスト・レスポンス編
 

Class Hierarchy

ここの部分
20160213010717

リクエスト受付からレスポンスまで

前回、サーバの立ち上げ時にルーティングが読み込まれるところまでを確認したので今回はリクエスト受付からレスポンスを返すまでを見てみる。

まずリクエストが来るとwsgiref/handlers.pyのrunが呼ばれ、Bottleの__call__が呼び出される
wsgiref/handlers.py

self.setup_environ()
# リクエストを処理しレスポンスを取得する
self.result = application(self.environ, self.start_response)
# レスポンスを返す
self.finish_response()

bottle.py

def __call__(self, environ, start_response):
    """ Each instance of :class:'Bottle' is a WSGI application. """
    return self.wsgi(environ, start_response)

リクエストを処理する際に、LocalRequestのbindを呼び出しBaseRequestのコンストラクタにてenvironとLocalRequestをセットする。これでBaseRequestのラッパーからパラメータを取得可能になる。レスポンスも同様にLocalResponseのbindを呼び出す。

try:
    out = None
    environ['bottle.app'] = self
    request.bind(environ)
    response.bind()
    try:
        self.trigger_hook('before_request')
    except HTTPResponse:
        return  _e()
    # ここでリクエストの主処理を実行する
    out = _inner_handle()
    return out;
finally:
    if isinstance(out, HTTPResponse):
        out.apply(response)
    self.trigger_hook('after_request')

BaseRequestのコンストラクタ

self.environ = {} if environ is None else environ
self.environ['bottle.request'] = self

リクエスト情報は以下のように取得可能になる

def index():
    request.forms.get('test')
    return 'TOP'

LocalRequestは以下の通りマルチスレッドに対応している

#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
#: request callback, this instance always refers to the *current* request
#: (even on a multi-threaded server).

hookポイントで以下のようにbefore_requestとafter_requestが利用可能

@hook('before_request')
def before_request():
    request.session = 'session'
@hook('after_request')
def after_request():
    print 'test'

_handleの_inner_handle()にてrouteのactionを呼び出しresponseを取得する

def _inner_handle():
    # Maybe pass variables as locals for better performance?
    try:
        route, args = self.router.match(environ)
        environ['route.handle'] = route
        environ['bottle.route'] = route
        environ['route.url_args'] = args
        return route.call(**args)

リダイレクトは以下のメソッドが用意されている

def redirect(url, code=None):
    """ Aborts execution and causes a 303 or 302 redirect, depending on
        the HTTP protocol version. """
    if not code:
        code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
    res = response.copy(cls=HTTPResponse)
    res.status = code
    res.body = ""
    res.set_header('Location', urljoin(request.url, url))
    raise res
Posted on

Python Bottleのソースを読む テンプレート編

前回に続きPythonの軽量WebフレームワークBottleのソースを読む テンプレート編
 

Class Hierarchy

ここの部分
20160206230621
 

Bottleのテンプレートについて

Bottleのテンプレートは、Simple、Cheetah、Jinja2、Makoの4種類があり、BaseTemplateを継承している
Pythonのテンプレートの種類(参考)
http://www.cmscom.jp/blog/af0ga8

テンプレートの使い方

テンプレートの拡張子には以下が利用できる

extensions = ['tpl', 'html', 'thtml', 'stpl']

例えばjinja2を使う場合は以下のように呼び出すことができる(jinja2をpip installする必要あり)

@route('/')
def jinja2():
    name = 'text'
    return jinja2_template('jinja2', name=name)

view

{{ name }}

テンプレート呼び出しの処理をみてみる

各テンプレートは以下のように設定されている。 functools.partialにfunc templateが渡されている

mako_template = functools.partial(template, template_adapter=MakoTemplate)
cheetah_template = functools.partial(template, template_adapter=CheetahTemplate)
jinja2_template = functools.partial(template, template_adapter=Jinja2Template)

jinja2_templateが呼ばれた時の処理

*argsにはjinja2が、**kwargsにはtemplate_adapter=Jinja2Templateが渡される

def template(*args, **kwargs):

引数からjinja2テンプレート名を取得

tpl = args[0] if args else None

Jinja2Templateを取得

adapter = kwargs.pop('template_adapter', SimpleTemplate)

テンプレートパスをlookup(パスは./views/か./)

lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)

Jinja2Templateをインスタンス化

TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)

BaseTemplateのコンストラクタの最後のprepareでJinja2Templateのprepareが呼ばれる

self.prepare(**self.settings)

ここではじめてjinja2がimportされる

from jinja2 import Environment, FunctionLoader

テンプレートのrenderメソッドにてレンダリングしviewを返す

return TEMPLATES[tplid].render(kwargs)

BottleのテンプレートはBaseTemplateを継承しprepareとrenderを実装することで使える仕組みになっている

def prepare(self, **options):
""" Run preparations (parsing, caching, ...).
It should be possible to call this again to refresh a template or to
update settings.
"""
raise NotImplementedError
def render(self, *args, **kwargs):
""" Render the template with the specified local variables and return
a single byte or unicode string. If it is a byte string, the encoding
must match self.encoding. This method must be thread-safe!
Local variables may be provided in dictionaries (args)
or directly, as keywords (kwargs).
"""
raise NotImplementedError