CORDEA blog

Android applications engineer

static method 等を呼び出しているテストケースで Robolectric の Shadow を使う

あんまり知られてないような知られてるような、そんな感じがしたので Robolectric の Shadow の使い道をちょっと紹介します。

robolectric.org

紹介するのは

  • static method を呼び出ている
  • kotlin の object 宣言がされた singleton を内部で直接使用している
  • 内部で new している

などなど、色々な記事で PowerMock の出番とされていそうなケースについてです。

今回はこれを Robolectric の Shadow で解決します。
前提として、私は PowerMock を入れようと思った時、それは設計を見直しをするべき時だと考えています。
とはいえ、古いアプリや大規模なアプリなどでテストは書きたいが static method の呼び出しがネックになっている、直す工数もない。しかし PowerMock は入れたくないというケースは稀ですが存在します。
そんな時、使うべきかは置いておいて Shadow が使えます。

いくつか例を紹介します。
この例は全て GitHub にあります (難読化された感じの雑な命名はあとで直します)
github.com

1 つ目

object AUtil {
    fun a() {
        throw IllegalArgumentException()
    }

    @JvmStatic
    fun b() {
        throw IllegalArgumentException()
    }
}

class A {
    fun a() {
        AUtil.a()
    }

    fun b() {
        AUtil.b()
    }
}

1 つ目の例はこれです。
こんなの存在しないと思いますが、テストからアクセスできない何かにアクセスして throw されてる、でもその後の処理がテストしたい...!みたいな、そんな感じで考えてください。

これに対しては、以下のように Shadow を定義できます。

@Implements(AUtil::class)
class ShadowAUtil {
    companion object {
        @JvmStatic
        @Implementation
        fun b() {
        }
    }

    @Implementation
    fun a() {
    }
}

これを使用するとテストを通過させることができます。

@RunWith(AndroidJUnit4::class)
@Config(shadows = [ATest.ShadowAUtil::class])
class ATest {
    @Test
    fun a() {
        A().a()
    }

    @Test
    fun b() {
        A().b()
    }

    ...
}

2 つ目

2 つ目は内部で new していて、なんか失敗しているケースです。
Shadow は constructor の呼び出しにも対応しています。

class D(d: String) {
    init {
        throw IllegalArgumentException()
    }
}

以下のような Shadow を定義します

@Implements(D::class)
class ShadowD {
    @Implementation
    fun __constructor__(d: String) {
    }
}

これを使用すると通過します
また、これは変な使い方として、

class B(private val b: String)

このようなクラスを Java 側から使用するとして

public class C {
    public void c() {
        new B(null);
    }
}

null を入れてしまうと当たり前ですが java.lang.IllegalArgumentException: Parameter specified as non-null is null ... が発生します
が、以下のような Shadow を定義すると回避できます。

@Implements(B::class)
class ShadowB {
    @Implementation
    fun __constructor__(b: String?) {
    }
}

使い所はありません。

3 つ目

実際に呼び出されたかどうか知りたい時、引数を assert したいときなど。ここまで来たら PowerMock 使えばという話なんですが、可能です。

@Implements(AUtil::class)
class ShadowAUtil {
    var isPassed = false

    @Implementation
    fun a() {
        isPassed = true
    }
}

とりあえずこんな感じで定義しました。
Robolectric は object に対応する Shadow を受け取ることができます。

@Test
@Config(shadows = [ShadowAUtil::class])
fun a() {
    val util = Shadow.extract<ShadowAUtil>(AUtil)
    assert(!util.isPassed)
    A().a()
    assert(util.isPassed)
}

便利です。

Shadow は多くの場面で活躍しますが、一方で何をしているのかが慣れるまで分かりづらいという欠点がありますので、用法用量を守って使用すると良さそうです。

Mackerel Client 1.2 をリリースしました

この度、MackerelClient 1.2 をリリースしましたので、リリースノートに書かなかった諸々を書きます。
ちなみに 1.1 (2016年4月24日) 以来、2 年半ぶりの更新となります。

Get it on Google Play

反映されるのはもう少し後かもしれません
スクリーンショット等はそのうち更新します

github.com

リリース内容

  • アラート一覧にホスト名や監視設定の値を表示するようにした
  • アラートの取得、グラフの描画を早くした
  • デザイン修正
    • card 部分のスタイルをちょっと変えた
    • グラフ部分のスタイルをちょっと変えた
  • バグ修正
    • ドロワーの文字色がいつの間にか毒々しくなってたので直した (というか直った)
  • min sdk version: 19 -> 21
  • target sdk version 23 -> 28

内部的な変更

  • 264 files changed, 8403 insertions(+), 6158 deletions(-)
  • ktlint の導入
  • dagger の導入
  • databinding の導入
  • groupie の導入
  • parcelize の導入
  • AndroidX 対応
  • 諸々のバージョンアップ
    • kotlin 1.0.1 -> 1.2.71
    • realm 0.87.2 -> 5.1.0

大規模な設計の見直しが入っており、差分がかなり大きくなりました。
グラフの描画部分など、主機能については大きく書き直しています。
まだ完全ではありませんが、まぁまぁまともになってきています。


気が向いたら使ってみてください。

Hy と Python の version について

Hy の install 時に Python version で少し困ったりしたのでメモ

普通に Python 3.7.0 の環境で Hy を pip install して実行すると

ImportError: invalid flags 1531398560 in 'hy.core.language'

こうなったり、または一回は実行できるけど二度目は失敗する、みたいなことになる

Hy 0.14.0 は Python 3.7.0 に対応していないためにこのエラーは起きているようで
これより後に 3.7.0 対応が行われている

と言うわけで対応としては公式通り、基本的には

$ pip install hy

よりも

$ pip install git+https://github.com/hylang/hy.git

の方が良さげ

Spacemacs の install で嵌ったりした

最近 Lisp 方言を書く機会が多いので重い腰を上げて Emacs を入れました
といっても素の Emacs を育てるには Vim に染まりすぎたので
Spacemacs という Emacs の distribution に頼ることにしました

嵌ったところ

公式にしたがって ~/.emacs.d に clone したのですが

Error (use-package): org-projectile :config: Symbol's function definition is void "projectile-global-mode"

的なことを言われたり言われなかったり、mode がうまく切り替わらなかったりして嵌ってました

で、spacemacs 自体を develop に切り替えることでうまくいきました
予想ですが、この PR が関係してそうだなと

$ cd ~/.emacs.d
$ git checkout develop

このあと一応 dotspacemacs/install し直してます

その他

思ったより Spacemacs 良かったです
C-x C-c と C-g だけ覚えとけばなんとかなる

Docker で mediawiki 立てて oauth extension 入れるまで

前も似たようなこと書いた気がするけど...
検証用に local で一時的に立ててるものなので色々ご注意ください。

build

docker hub から引っ張ってくると mediawiki が古い (多分) ので公式 repository を clone して build します

$ git clone https://github.com/wikimedia/mediawiki-docker.git
$ docker build --rm -t mediawiki:latest mediawiki-docker/stable/

run

立てます、動けばいいので色々適当です

$ docker run -it -d -e MYSQL_ROOT_PASSWORD="root" --name mysql mysql
$ docker run -it -d -e MEDIAWIKI_DB_PASSWORD="root" -e MEDIAWIKI_DB_TYPE="mysql" -p 8080:80 --link mysql:mysql --name mediawiki mediawiki:latest

ここからは http://localhost:8080 にアクセスしつつ色々進めます。
途中に入れる db への接続先は

$ docker inspect -f '{{ .NetworkSettings.IPAddress }}' mysql

最後まで行くと LocalSettings.php 入れてねって言われます。

local settings

今回は oauth extension をとにかく早く動かしたかったので mail 系を全部無効にしました (しないと認証求められるので)

$wgEnableEmail = false;
$wgEnableUserEmail = false; # UPO

...

$wgEmailAuthentication = false;

入れます

$ docker cp ./LocalSettings.php mediawiki:/var/www/html/

oauth extension

OAuth extension を download してきます
ここら辺です

Download MediaWiki extension - MediaWiki

適当なところに copy して解凍します

$ docker cp ./OAuth-*.tar.gz mediawiki:/tmp/
$ docker exec mediawiki tar xzf /tmp/OAuth-*.tar.gz -C /var/www/html/extensions

LocalSettings.php に設定を追記して update します
とりあえず誰でも consumer を追加できるようにしたいのでそんな感じにします

$ docker exec mediawiki sh -c "echo \"wfLoadExtension( 'OAuth' );\" >> /var/www/html/LocalSettings.php"
$ docker exec mediawiki bash -c "echo $'\$wgGroupPermissions[\'user\'][\'mwoauthproposeconsumer\'] = true;' >> /var/www/html/LocalSettings.php"
$ docker exec mediawiki php /var/www/html/maintenance/update.php

consumer registration は http://localhost:8080/index.php/Special:OAuthConsumerRegistration

BitBucket で OAuth 1 での認証がうまく行かなかった話

結論

timestamp が float だった

経緯

私が作っている nim の oauth library を修正したところ
bitbucket の例がうまく動かなかった (というか bitbucket は多分ずいぶん昔から動いていなかった) ので調査していて分かったのでメモ

問題の切り分けに時間がかかった要因としては

の例は動いていた、という点があります。

概要

いくつかの bitbucket x oauth 1 の例から正常に動く以下の例を参考に

Bitbucket OAuth 1 Tutorial — Requests-OAuthlib 1.0.0 documentation

送られているヘッダーを見比べた結果

  • Requests-OAuthlib
 oauth_timestamp=\"1530407158\", 
  • oauth
oauth_timestamp=\"1530407309.0\", 

となっていて、ここが原因でした。
先ほどの Twitter 等のサービスは、timestamp が float でも受け入れていたということのようです。
わかれば単純ですけど、bitbucket は理由を返してくれないので何故こけてるのかが分からずかなり時間がかかりました...

Red で http request を行う方法

Red programming language で http request を行う方法がちょっと分かりづらかったのでメモ

GET

本当にシンプルな GET

>> read http://localhost:8080/api
== {{"status":"ok"}}

header の追加など必要な場合

>> write http://localhost:8080/api [GET [
    Accept: "application/json"
] ""]
== {{"status":"ok"}}

(読みやすいように実際の出力を一部修正してます)

POST

>> write http://localhost:8080/api [POST [
    Accept: "application/json"
] {{"data": "data"}}]
== {{"status":"ok"}}

Others

他は一緒です。
ただ、PATCH と DELETE は 0.6.3 時点で対応されていないのでご注意ください。

>> write http://localhost:8080/api [PUT [
    Accept: "application/json"
] ""]
== {{"status":"ok"}}
>> write http://localhost:8080/api [PATCH [
    Accept: "application/json"
] ""]
*** Internal Error: reserved for future use (or not yet implemented)
*** Where: write
*** Stack:  

>> write http://localhost:8080/api [DELETE [
    Accept: "application/json"
] ""]
*** Internal Error: reserved for future use (or not yet implemented)
*** Where: write
*** Stack:

refinement

read/write は refinement による挙動の違いがあります。

refinement の一覧はこの辺りで確認できます
read, write 共に binary, lines, info refinement が実装されてます
ちなみに write は append と part もエラー発生せずに通るのですが、みた感じ実装されてない気がします

binary

データを 16 進形式で返します

>> read/binary http://localhost:8080/api
== #{7B22737461747573223A226F6B227D}
lines

文字列を改行で区切って block で返します

>> read/lines http://localhost:8080/api
== ["text" "text" "text"]
info

名前の通り

>> info: read/info http://localhost:8080/api
== [200 #(
    X-Content-Type-Options: "nosniff"
    Server: "WEBrick...
>> print info
200 X-Content-Type-Options: "nosniff"
Server: "WEBrick/1.3.1 (Ruby/2.4.2/2017-09-14)"
Content-Length: "15"
Content-Type: "application/json"
Connection: "close"
Date: "Sun, 03 Dec 2017 09:47:41 GMT" {"status":"ok"}

詳しいことは実装見るのが一番早いので、simple-io あたりをご覧ください。