ReactとGoogle Sign-In
Google Sign-In をReactなSPAで使った時の日記です。
サーバーとの連携やセッションの管理について少し悩んだのでメモっておきます。ちょっと長くなりそうなので何回かに分けます。
Google Sign-In
Google Sign-Inと言っているのはこれのこと。 developers.google.com
こちらにしたがって、Google Cloud Platformの認証情報を作成し、クライアントIDを取得する。「承認済みの JavaScript 生成元」として localhost:3000
を指定しておく。
取得できたら早速使ってみる。まずはReactなしで試す。必要なのは以下の4つ。
- metaタグに取得したクライアントIDを指定する
https://apis.google.com/js/platform.js
を読み込むscriptタグを用意するg-signin2
というclass属性のついたdivタグを用意する- 上のdivタグに
data-onsuccess
属性でログイン成功時のコールバックを指定する
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>sample</title> <meta name="google-signin-client_id" content="<CLIENT_ID>"> <script src="https://apis.google.com/js/platform.js" async defer></script> </head> <body> ようこそ! <div class="g-signin2" data-onsuccess="onSignIn"></div> <script> function onSignIn(googleUser) { var profile = googleUser.getBasicProfile(); console.log('ID: ' + profile.getId()); console.log('Name: ' + profile.getName()); console.log('Image URL: ' + profile.getImageUrl()); console.log('Email: ' + profile.getEmail()); } </script> </body> </html>
<CLIENT_ID>
はGoogle Cloud Platformのコンソールで取得した値にすること。適当なWebサーバーを3000ポートで起動しブラウザで localhost:3000
にアクセスする。
ログインボタンをクリックしてGoogleアカウントでログインする。ブラウザのコンソール出力を見ると以下のような出力が。
ID: 111 Name: hoge Image URL: https://lh4.googleusercontent.com/fuga/photo.jpg Email: foo.bar@gmail.com
おぉー、簡単!
Reactと一緒に使う
ReactアプリケーションでGoogle Sign-Inを使ってみる。
create-react-appでアプリケーションの雛形を作成する。使った create-react-app
のバージョンは
$ create-react-app --version 1.5.2
package.jsonのdependencies。
"dependencies": { "react": "^16.3.0", "react-dom": "^16.3.0", "react-scripts": "1.1.1" },
雑にさっきのコードをsrc/App.js
に移植する。jsxでは class
属性は className
と書くのでそこは直しておく。
class App extends Component { onSignIn = (googleUser) => { var profile = googleUser.getBasicProfile(); console.log('ID: ' + profile.getId()); console.log('Name: ' + profile.getName()); console.log('Image URL: ' + profile.getImageUrl()); console.log('Email: ' + profile.getEmail()); } render() { return ( <div> ようこそ! <div className="g-signin2" data-onsuccess="onSignIn"></div> </div> ) } }
$ npm start
して、 localhost:3000
にアクセス。
む、ログインボタンが表示されない。metaタグとscriptタグを追加していなかった。public/index.html
にmetaタグとscriptタグを追加する。
--- a/public/index.html +++ b/public/index.html @@ -19,6 +19,8 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> + <meta name="google-signin-client_id" content="<CLIENT_ID>"> + <script src="https://apis.google.com/js/platform.js" async defer></script> <title>React App</title> </head> <body>
表示された。ログインしてブラウザのコンソール出力を見ると...
何も表示されない。 data-onsuccess="onSignIn"
で指定したコールバックが呼ばれないですね。まぁそりゃそうだなぁ。
react-google-login
どうしよっかなぁと思ってGitHubを眺めてるとreact-google-loginというのを発見。
試してみたけど、ログインボタンをrenderするときにクライアントIDを指定する方式になっていて、ログイン済みの状態でアクセスした時にログアウトボタンだけ欲しい場合に都合が悪かったので使わないことにする。
JavaScriptコードからログインボタンを作成する
Googleのドキュメントを読んでみると、自分のJavaScriptコードでログインボタンを作成できることが分かる。この時にコールバックを指定できる。この方法を使えば以下のような感じでログインボタンを描けそう。
gapi.signin2.render('google-signin-button', { 'onsuccess': this.onSignIn, 'onfailure': (err) => console.error(err) });
さらにリファレンスを見ると、metaタグでclient_idを指定してたけど、これもJavaScriptで指定できるようだ。
gapi.load('auth2', () => { gapi.auth2.init({client_id: <CLIENT_ID>}) .then( (result) => console.log(result), (err) => console.error(err) ); })
これらを使って、 initSignInButton
メソッドを追加する。
--- a/src/App.js +++ b/src/App.js @@ -1,6 +1,20 @@ import React, { Component } from 'react'; class App extends Component { + initSignInButton = (gapi) => { + gapi.load('auth2', () => { + gapi.auth2.init({client_id: <CLIENT_ID>}) + .then( + (result) => { + gapi.signin2.render('google-signin-button', { + 'onsuccess': this.onSignIn, + 'onfailure': (err) => console.error(err) + }); + }, + (err) => console.error(err) + ); + }) + } onSignIn = (googleUser) => { var profile = googleUser.getBasicProfile(); console.log('ID: ' + profile.getId()); @@ -26,7 +42,7 @@ class App extends Component { return ( <div> ようこそ! - <div className="g-signin2" data-onsuccess="onSignIn"></div> + <div id='google-signin-button'></div> </div> ) }
あとは gapi
オブジェクトをどうやって取得するか。Googleのドキュメントのサンプルにある <script src="https://apis.google.com/js/platform.js?onload=renderButton" async defer></script>
というところ。scriptタグでGoogleのjsファイルを読み込んでるところをどう書き直そう(npmになさそう・・・)。先ほどの react-google-login
のコードを調べてみると、DOMをごにょごにょしてscriptタグを動的に書き込んでた。それを真似する。
--- a/src/App.js +++ b/src/App.js @@ -1,6 +1,19 @@ import React, { Component } from 'react'; class App extends Component { + componentDidMount() { + this.downloadGoogleScript(this.initSignInButton) + } + downloadGoogleScript = (callback) => { + const element = document.getElementsByTagName('script')[0]; + const js = document.createElement('script'); + js.id = 'google-platform'; + js.src = '//apis.google.com/js/platform.js'; + js.async = true; + js.defer = true; + element.parentNode.insertBefore(js, element); + js.onload = () => callback(window.gapi); + } initSignInButton = (gapi) => { gapi.load('auth2', () => { gapi.auth2.init({client_id: <CLIENT_ID>})
public/index.html
に追記したタグは不要になったので消す。
--- a/public/index.html +++ b/public/index.html @@ -19,8 +19,6 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - <meta name="google-signin-client_id" content="<CLIENT_ID>" - <script src="https://apis.google.com/js/platform.js" async defer></script> <title>React App</title> </head> <body>
localhost:3000
にアクセスして、ログインしてみると...うまくいった!
おしまい
次回は、react-routerでログイン前と後で画面分けたりする、予定。
最終的に create-react-app
した後に編集したのは src/App.js
だけ。
JUnit実践入門のMockitoの部分をJMockit (version 1.37) でやってみた
Java アドベントカレンダー 2017 5日目の記事です。
以前書いた記事のJMockit のバージョンが古くていつか更新したいなぁと思っていたので更新しました!
この記事は、JUnit実践入門のMockitoについて説明してる部分をJMockitでやってみました、という内容です。
JMockitはJUnitなどでテストを書くときに利用できるモックライブラリです。今回使ったJMockitのバージョンは1.37、JUnitのバージョンは4.12です。
準備
build.gradleに書きます。
testCompile 'org.jmockit:jmockit:1.37' testCompile 'junit:junit:4.12'
書く順番に要注意なんてことが書いてあるので気をつけましょう・・・。
http://jmockit.org/gettingStarted.html#library
あとは、JUnitのテストクラスに @RunWith(JMockit.class)
をつけます。
公式のチュートリアルはこちら。
http://jmockit.org/tutorial.html
スタブメソッドの定義
早速、以前のバージョンと違いがあります。 NonStrictExpectations
がなくなったようです。代わりっぽいのがないので Expectations
を使います。
@Mocked List<String> stub; @Test public void スタブメソッドの定義() { new Expectations() {{ stub.get(0); result = "Hello"; // スタブメソッドの定義 }}; assertEquals(stub.get(0), "Hello"); }
例外を送出するスタブメソッド
以前は NonStrictExpectations
を使ったサンプルコードを書いてました。 NonStrictExpectations
の場合は呼び出し回数まで厳密に見ないのですが、 Expectations
は定義したスタブ実装が呼び出されないとテストが失敗してしまいます。そのため、以下のテストは失敗します。
@Test(expected = IndexOutOfBoundsException.class) public void 例外を送出するスタブメソッド() throws Exception { new Expectations() {{ // NonStrictExpectationsが使えなくなった // Expectationsは定義した内容が呼ばれないとテストが失敗してしまう stub.get(0); result = "Hello"; stub.get(1); result = "World"; stub.get(2); result = new IndexOutOfBoundsException(); }}; stub.get(2); }
失敗した時のメッセージは以下のようになります。
Caused by: Missing 1 invocation to: java.util.List#get(0) on mock instance: $Impl_List@5a39699c instead got: java.util.List#get(2) at SampleTest$2.<init>(SampleTest.java:38) at SampleTest.例外を送出するスタブメソッド(SampleTest.java:33) Caused by: Missing invocations at SampleTest$2.<init>(SampleTest.java:36) at SampleTest.例外を送出するスタブメソッド(SampleTest.java:33) Caused by: java.lang.IndexOutOfBoundsException at SampleTest.例外を送出するスタブメソッド(SampleTest.java:40)
以下のように、呼び出さないスタブ実装を消してあげればテストが通ります。
@Test(expected = IndexOutOfBoundsException.class) public void 例外を送出するスタブメソッド() throws Exception { new Expectations() {{ // NonStrictExpectationsが使えなくなった // Expectationsは定義した内容が呼ばれないとテストが失敗してしまう // stub.get(0); result = "Hello"; // stub.get(1); result = "World"; stub.get(2); result = new IndexOutOfBoundsException(); }}; stub.get(2); }
void型を返すスタブメソッド
@Test(expected=RuntimeException.class) public void メソッドの戻り値がvoidのスタブメソッド() { new Expectations() {{ stub.clear(); result = new RuntimeException(); }}; stub.clear(); }
任意の引数に対するスタブメソッド
メソッドの引数にスタブオブジェクトを指定する方式に書き換えます。以前は引数に指定する場合は @Mocked
アノテーションがなくても大丈夫だったのですが、
@Test public void 任意の整数に対するスタブメソッド(/* Mockedアノテーションが必要 */ @Mocked final List<String> stub) { new Expectations() {{ stub.get(anyInt); result = "Hello"; }}; assertEquals(stub.get(0), "Hello"); assertEquals(stub.get(1), "Hello"); assertEquals(stub.get(999), "Hello"); }
ちなみに @Mocked
をつけなかった時のメッセージは以下なので、気がつかないとハマりそうです。。
java.lang.Exception: Method 任意の整数に対するスタブメソッド should have no parameters
スタブメソッドの検証
特に変わらず Verifications
は使えました。
@Test public void スタブメソッドの検証(@Mocked final List<String> mock) { mock.clear(); mock.add("Hello"); mock.add("Hello"); new Verifications() {{ mock.clear(); times = 1; mock.add("Hello"); times = 2; mock.add("World"); times = 0; }}; }
Exceptations
を使う方法も変わらず。
@Test public void スタブメソッドの検証_Exceptationsで指定(@Mocked final List<String> mock) { new Expectations() {{ mock.clear(); times=1; mock.add("Hello"); times=2; mock.add("World"); times=0; }}; mock.clear(); mock.add("Hello"); mock.add("Hello"); }
部分的なモックオブジェクト
これがうまくいきませんでした! ArrayList
の size()
だけ書き換えたい場合です。
以下のページを読むとこれで良さそうなのですが、 StackOverflowError
が起きてしまいました。原因が分からず・・・。
http://jmockit.org/tutorial/Faking.html
@Test public void 部分的なモックオブジェクト() { new MockUp<ArrayList<String>>() { @Mock public int size() { return 100; } }; final List<String> mock = new ArrayList<>(); mock.add("Hello"); assertEquals(mock.get(0), "Hello"); assertEquals(mock.size(), 100); }
2017/12/13 追記
ちなみに、ArrayListではなく、例えばStackを使った場合はうまくいきました。。
@Test public void 部分的なモックオブジェクト() { new MockUp<Stack<String>>() { @Mock public int size() { return 100; } }; final Stack<String> mock = new Stack<>(); mock.push("Hello"); assertEquals(mock.firstElement, "Hello"); assertEquals(mock.size(), 100);
スパイオブジェクト
こちらは以前から特に変わらず。
private static class SpyExample { Logger logger = Logger.getLogger(this.getClass().getName()); public void doSomething() { logger.info("doSomething"); } } @Test public void JMockitのMockUpAPIを使ったテスト() { // Setup SpyExample sut = new SpyExample(); final StringBuilder infoLog = new StringBuilder(); new MockUp<Logger>() { @Mock public void info(Invocation invocation, String message) { infoLog.append(message); invocation.proceed(); } }; sut.logger = Logger.getLogger(this.getClass().getName()); // Exercise sut.doSomething(); // Verify assertEquals(infoLog.toString(), "doSomething"); }
おしまい
うーむ、部分的に書き換える方法がうまくいかないのは悔しいですね。また時間を見つけて調べてみたいと思います!
MobX + Babel + webpack + Flow
地元の勉強会のための準備です。
前提
$ node -v v8.2.1 $ npm -v 5.3.0
準備。
$ npm init
MobX
インストールします。
$ npm i mobx -D
MobXを使ったストアのサンプル。このチュートリアル分かりやすいです。
MobX: Ten minute introduction to MobX and React
import { autorun, observable, action, computed } from 'mobx'; export default class TodoStore { @observable todos = []; @observable pendingRequests = 0; constructor() { autorun(() => console.log(this.report)); } @computed get completedTodosCount() { return this.todos.filter( todo => todo.completed === true ).length; } @computed get report() { if (this.todos.length === 0) { return "<none>"; } else { return `Next todo: "${this.todos[0].task}". ` + `Progress: ${this.completedTodosCount}/${this.todos.length}`; } } @action addTodo(task) { this.todos.push({ task, completed: false, assignee: null }); } }
デバッガー
mobx-remotedevを使います。
GitHub - zalmoxisus/mobx-remotedev: MobX DevTools extension
上のリンク先にも書いてある通り、まずはChromeの拡張機能である redux-devtools-extension をインストールします。
GitHub - zalmoxisus/redux-devtools-extension: Redux DevTools extension.
mobx-remotedevをインストールします。
$ npm i mobx-remotedev -D
TodoStoreに @remotedev
を追記します。
import remotedev from 'mobx-remotedev/lib/dev'; @remotedev export default class TodoStore { ・・・
babel
上のコードには、export、class、import、getter、デコレータなどが使われています。当然このままではブラウザでは動かないので、Babelでコンパイルする必要があります。
$ npm i babel-cli babel-preset-es2015 babel-preset-stage-1 babel-plugin-transform-decorators-legacy babel-plugin-transform-class-properties -D
.babelrcを用意します。
{ "presets": [ "es2015", "stage-1" ], "plugins": [ "transform-decorators-legacy", "transform-class-properties" ] }
package.jsonに build
タスクを定義してbabelを実行するようにします。
"scripts": { "build": "babel src -d dist" }
これでコンパイルできます。
$ npm run build
webpack
次は、webpackを使って依存関係を解決したjsファイルを作ります。
$ npm i webpack babel-loader -D
loaderとして babel-loader を使用する。 .babelrc を削除して webpack.config.js を作る。
module.exports = { context: __dirname + "/src", entry: { "application": "./index.js" }, output: { path: __dirname + "/dist", filename: "[name].js" }, module: { rules: [ { exclude: /node_modules/, loader: require.resolve("babel-loader"), options: { plugins: [ "transform-decorators-legacy", "transform-class-properties" ], presets: ["es2015", "stage-1"] } } ] } }
webpackのentryとなるjsファイルを用意する。
import TodoStore from './TodoStore'; const observableTodoStore = new TodoStore(); window.observableTodoStore = observableTodoStore;
package.jsonの build をbabelからwebpackを使うように変更する。
"scripts": { "build": "webpack" }
これでBabelを使ってコンパイルして、webpackを使って依存関係を解決したJavaScriptファイルを作成できる。
$ npm run build
おしまい
dist/application.js
として出力されるので、これをhtmlで読み込めばTodoStoreを使えます!
package.jsonはこんな状態。
{ "name": "mobx-todo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "webpack" }, "devDependencies": { "mobx": "^3.2.1", "babel-cli": "^6.24.1", "babel-loader": "^7.1.1", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-1": "^6.24.1", "mobx-remotedev": "^0.2.8", "webpack": "^3.3.0" } }
Flow (おまけ)
Flowで型チェックしてみる。
$ npm i flow-bin flowtype babel-preset-flow -D
TodoStoreを書き換える。
// @flow import { autorun, observable, action, computed } from 'mobx'; import remotedev from 'mobx-remotedev/lib/dev'; type Task = { task: string, completed: boolean, assignee: ?string, } @remotedev export default class TodoStore { @observable todos: Array<Task> = []; @observable pendingRequests: number = 0; constructor() { autorun(() => console.log(this.report)); } @computed get completedTodosCount(): number { return this.todos.filter( todo => todo.completed === true ).length; } @computed get report(): string { if (this.todos.length === 0) { return "<none>"; } else { return `Next todo: "${this.todos[0].task}". ` + `Progress: ${this.completedTodosCount}/${this.todos.length}`; } } @action addTodo(task: string): void { this.todos.push({ task, completed: false, assignee: null }); } }
webpack.config.jsを書き換える。
@@ -15,9 +15,10 @@ module.exports = { options: { plugins: [ "transform-decorators-legacy", - "transform-class-properties" + "transform-class-properties", + "transform-flow-strip-types" ], - presets: ["es2015", "stage-1"] + presets: ["es2015", "stage-1", "flow"] } } ]
package.jsonにflowタスクを追加します。
"scripts": { "build": "webpack", "flow": "flow" }
Flowで型チェック
$ npm run flow