投稿者「shoarai」のアーカイブ

LM386でギターアンプを作った

日頃、仕事やプライベートでプログラミングをしていますが、今回始めて電子工作でギターアンプを作りました。

注意事項

電子工作は工具の扱いや配線作業において危険がともなうため、怪我などに十分ご注意ください。本記事の内容を参考にして発生した事故などは、shoaraiは一切責任を負いません。自己責任でお願いいたします。

作りたいもの

ギターは持っているのですが、アンプは随分前に売ってしまい、PCに繋いで音を出していました。ただ毎回PCに繋ぐのは面倒なので、簡単に音出ししたいときのために、ギターに直差しできるような小型のアンプがほしいと思っていました。でもギターに直差しできて、かつスピーカーがついているアンプが売ってない。。ヘッドフォンを繋いで使うようなミニアンプはあるんですが。そこで、自分で作れないか調べたところ、わりと簡単にしかも安く作れるようなので試してみました。

作りたいもののイメージは以下の通り。

  • ちょっと弾きたいときに、ギターに直差しして使える。
  • 自分が聞こえればいいので、音量は小さくてもいい。
  • 音色は幅広く、クリーンも歪みも出したい。歪み具合は調整したい。

作ったもの

こちらが作成したギターアンプです。左上のトグルスイッチは電源、左下はゲインのON/OFF切り替えです。右上のつまみは音量、右下はゲインの調整になっています。ギターに直差ししたときに、右手でスピーカーを隠さずにつまみを調整できるように、右側につまみがくるように配置しました。電源を入れると青色LEDが光ります。ステレオジャックがあり、短いパッチケーブルを使ってギターと接続することができます。

ステレオジャックではなくステレオプラグを取り付けてギターに直差しできるようにすることも考えましたが、卓上に置いても使えるようにステレオジャックを付けました。

後で部品を交換したり修理したりできるように、グルーガンなど使わずにナットで部品を締めて取り付けました。

基盤上に部品をキツキツに詰めてますが、今後拡張したくなったときのためにまだカットしていません。

回路図

4.7kΩ220μF100Ω10μF10μF9V0.047μF220μF1kΩB100ΩB8Ω0.5WLM386N-1Blue LED154236876.3mmStereo Jack

回路にはLM386という小型パワーアンプICを使っています。抵抗やコンデンサなどの構成はほとんど公式のデータシートに記載されているものと同じです。違いとしては、音量とゲイン調整用のスイッチとボリューム、電源の状態確認用のLEDと抵抗をつけたぐらいです。また、5番ピンにある抵抗をデータシート記載の10Ωで試したところ、なぜか手前のコンデンサが異常に熱くなったので、100Ωに変えました。セラミックコンデンサを使っているからなのか、原因はわからず。LEDに繋ぐ抵抗は1kΩ、2.2kΩあたりも試したのですが、LEDの光が眩しすぎたので4.7kΩにしました。

振り返り

スピーカーをケースに取り付ける前と後で音を比べると、取り付けた後は少しこもった音になってしまいました。ケース本体の素材はポリプロピレン、蓋はポリエチレンなので、素材が原因なのか、ケースの穴が空いていない部分が影響しているのかわかりませんが、改善したいところです。スピーカーを覆っている部分をすべて穴開けしてもいいかもしれません。

配線が少し複雑になってしまった気がします。基盤のレイアウトを考えるときに、ケースに取り付けた部品の位置を考慮して配線しやすいようにすればよかったと思いました。

ボリュームのシャフトが少し長すぎるため、つまみからはみ出てしまっています。短く切るのは面倒ですし道具もないので、ボリュームとケースの間に何か挟んで高さを調整すれば解消するかも。

最後に、お気づきでしょうか?実はスピーカーを固定しているネジが1本足りません笑。これは買ってあったスピーカー抑え金具を急遽使ったのですが、そのネジが3本しかなかったためです。できれば4箇所とも別のネジを使いたいところです。

補足

初めてギターアンプを作るにあたって今回購入したものを以下の記事にまとめました。参考まで。

KotlinでToastをカスタマイズする

先日、ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。このページではその中でもポップアップであるToastをカスタマイズして表示する処理について紹介します。

標準のToastを表示する

標準のToastは以下のコードで表示できます。

object ToastView {
    fun showToast(context: Context, message: String) {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
}

Toastをカスタマイズする

前述のような標準のToastだと目立たず、通知を見逃しやすいので、上の画像のようにToastのサイズやフォントサイズが大きい独自のレイアウトを作成します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:weightSum="1">

    <TextView
        android:id="@+id/message"
        android:layout_width="320dp"
        android:layout_height="80dp"
        android:background="@drawable/toast_shape"
        android:gravity="center"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textColor="@color/colorBase"
        android:textSize="40sp" />

</LinearLayout>

上記の独自レイアウトを表示します。

object ToastView {
    fun showToast(context: Context, message: String) {
        val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val view = inflate.inflate(R.layout.toast, null)
        val textView = view.findViewById(R.id.message) as TextView
        textView.text = text

        Toast(context).run {
            this.view = view
            duration = Toast.LENGTH_LONG
            setGravity(Gravity.BOTTOM, 0, 250)
            show()
        }
    }
}

KotlinでAndroidの時刻を検知する

先日、ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。このページではその中でも時刻の検知の処理について紹介します。

アラームを開始、停止する

Androidアプリで、ある時刻になったら処理を実行したい場合、AlarmManagerを使用します。厳密には、ある時刻になったことを通知するのではなく、ある時間を経過したことを通知できます。
今回はアラームアプリなので、ある程度正確な時間を通知できるsetExact()を呼びます。通知はBroadcastReceiverで受け取ります。
以下のクラスでは、アラームを開始して1度だけ通知します。別のアラームがすでに実行中なら、実行中のアラームは停止し、新しいアラームが開始されます。

object OnceAlarmManager {
    private const val REQUEST_CODE = 0
    private var alarmManager: AlarmManager? = null
    private var pendingIntent: PendingIntent? = null

    fun startAlarm(context: Context, calendar: Calendar) {
        alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val intent = Intent(context, AlarmBroadcastReceiver::class.java)
        pendingIntent = PendingIntent.getBroadcast(
                context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        alarmManager?.setExact(AlarmManager.RTC, calendar.timeInMillis, pendingIntent)
    }

    fun stopAlarm() {
        alarmManager?.cancel(pendingIntent)
    }
}

アラームアプリでは、1つのアラームで時刻通知したら次のアラームを開始する必要があるので、以下の通り、設定がONになっているアラームを開始します。

object WeeklyAlarmManager {
    fun startNextAlarm(context: Context) {
        var weeklyAlarms = WeeklyAlarmDataManager.weeklyAlarms
        if (WeeklyAlarmUtil.hasPowerOn(weeklyAlarms)) {
            val calendar = WeeklyAlarmUtil.getNextAlarmAsCalendar(weeklyAlarms)
            OnceAlarmManager.startAlarm(context, calendar)
        } else {
            OnceAlarmManager.stopAlarm()
        }
    }
}

アラーム通知を受け取る

前述の通り、BroadcastReceiverでアラーム通知を受け取ります。通知を受け取ったら、ポップアップを表示し、次のアラームを開始します。

class AlarmBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Show popup.
        WeeklyAlarmManager.startNextAlarm(context)
    }
}

BroadcastReceiverをアプリに登録します。

<application
    ...
    <receiver android:name=".AlarmBroadcastReceiver">
    </receiver>
</application>

端末の再起動時やタイムゾーンの変更時に、アラームを再設定する

端末が再起動されたことやタイムゾーンが変更されたことを、BroadcastReceiverで受け取れるよう、actionを設定します。

<application
    ...
    <receiver android:name=".AlarmBroadcastReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.TIMEZONE_CHANGED" />
            <action android:name="android.intent.action.TIME_SET" />
            <action android:name="android.intent.action.DATE_CHANGED" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.PACKAGE_REPLACED" />
            <data
                android:path="com.isolity.toastalarm"
                android:scheme="package" />
        </intent-filter>
    </receiver>
</application>

上記で設定したactionの通知時にも、BroadcastReceiverのonReceive()が実行されます。アラーム通知の場合のみ(ここではactionがnullのとき)、ポップアップメッセージを表示します。

class AlarmBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == null) {
            // Show popup.
        }
        WeeklyAlarmManager.startNextAlarm(context)
    }
}

改善点

上記コードの改善点として、例えば、アラーム通知の判定がaction==nullだとわかりにくいので、アクション名つけた方がいいですね。あとはDIとかテストの方法はこれから調べます。

※Huaweiのスマホでの注意事項

Huaweiのスマホでは、以下の設定をしないと、アプリをKillしたあとにAlarmManagerの処理が止まってしまい、通知を受け取れなくなってしまいます。
設定 > 詳細設定 > バッテリーマネージャー > 保護されたアプリ > 対象のアプリをONにする
アプリごとに設定が必要なので、アプリを初めてインストールしたときに設定しておきます。僕はHuaweiのP8liteというスマホを使っていて、Emulatorでは動くのに実機で動かず非常に苦労しました。。Android開発者は各端末の知識も必要で大変ですね;

KotlinでAndroidアラームアプリを作った

Androidのアラームアプリを作ったときにやったこと、調べたことを紹介します。

作ったアプリ

ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。
ストアに公開したので、ぜひ触ってみてください!

やったこと

Cordova/PhoneGapでAndroidアプリを作ったことはありますが、ネイティブで作るのは初めてなので、まずは言語仕様の理解から始めました。

  1. Kotlinの理解
  2. 開発環境の構築
  3. 実装
  4. アイコンの作成
  5. リリース

Kotlinの理解

開発言語として、JavaではなくKotlinを選びました。
型推論が欲しいこと、Null安全に興味があったこと、周囲の評判が良かったことが理由です。
※先日、KotlinがAndroidアプリ公式の開発言語に選ばれましたが、僕が選んだ後の話だったのでニュースを見たときは幸せを感じました笑
言語仕様は、その言語を書き始める前にざっと知っておいた方が効率がいいと思っているので、以下の本を読みました。

Kotlinスタートブック

その他、実装中にリスト操作について知りたいことが多かったので、以下の記事を参考にさせていただきました。

Qiita:Kotlin のコレクション使い方メモ

開発環境の構築

Android Studioのインストールは公式サイトより。
Android StudioへのKotlinの導入は、前述のKotlin本を参考にしました。取り急ぎこれで十分かと。
JavaのコードをコピペするとKotlinコードに自動的に変換されるのは、Kotlinの大きなメリットでもありますね。

実装

実装した主な機能は以下の通り。

Androidネイティブの実装は初めてということもあり、だいぶ詰まりました…
細かい部分も含めて、別ページで紹介しようと思います。

広告の実装

アプリにバナー広告を実装してみました。使ったのはGoogleのAdMobです。手順は公式サイトを参考のこと。Android Studioと連携できるのでほぼ自動で実装できました。

アイコンの作成

アイコンの作成には、Inkscapeという無料の画像編集ソフトを使いました。

リリース

リリースの際、APKに署名する必要がありますが、これもAndroid Studioからできます。
むしろ、アプリの説明文を記載するのがなかなか大変ですね。

かかった時間

ざっくりですが、開発にかかった実時間の内訳は以下のような感じ。
といっても期間としては3ヶ月ぐらいかかってます。途中、実装に詰まって手が止まりました;

  • Kotlinの理解 → 1日
  • 開発環境の構築 → 1日
  • 実装 → 2週間
  • アイコンの作成 → 半日
  • リリース → 1日

振り返り

Contextの扱いであったりResourceファイルの違いであったり、Android開発の知識が乏しく実装中に手が止まることが多かったです。実装する前にAndroidの基礎本を読んでおけばよかったかなと。
試しに広告を入れてみましたが、はじめから広告ありきで画面設計、実装しておけば、広告を表示する隙間をあとで開ける必要はなかったかなと。

IonicアプリのCordovaをアップデートする

久しぶりにGoogle Play Developer Consoleを見たら、アラートが表示されていました。
PhoneGap version alert
アラートが出ているのはIonicで作ったアプリです。このアプリが使用しているCordovaのバージョンに脆弱性があるようで、期限内にアップロードしてね、という通知でした。

Ionicのアップデート

Ionicが依存するCordovaのバージョンは、Ionicのバージョンによって決まります。よってIonicをアップデートする必要があります。

$ npm i -g ionic

アプリのアップデート

Ionicをアップデートしても、アプリはアップデートされないため、一度アプリを消してから作り直す必要があります。まずはアプリを削除します。

$ ionic platform rm android

アプリをビルドし直します。

$ ionic build android

今後も脆弱性が見つかるかもしれないので、都度アップデートする必要がありますね。

C++によるベクトルと行列の計算クラス

大学の研究で運動機構のシミュレーションをしていた時に作った、ベクトルや行列の計算をするクラスを紹介します。GitHubにあるのでダウンロードしてみて下さい。
https://github.com/shoarai/arith

使い方

ベクトルの計算

ベクトルを含んだ計算を演算子のオーバーロードを使って実現することで、数式のようにスッキリと書けます。例では3次元ベクトルを計算していますが、z成分の値を0にすれば2次元ベクトルの計算も問題なくできます。

#include "arith/src/arith.h"
using namespace arith;
int main()
{
    // 初期化
    Vector vecA;              // x=0, y=0,  z=0
    Vector vecB(1, 10, 100);  // x=1, y=10, z=100
    // 要素の設定
    vecA.set(1, 2, 3);
    // 要素の取得
    double x = vecA.getx();
    double y = vecA.gety();
    double z = vecA.getz();
    Vector vecC;
    // ベクトル同士の加減算
    vecC  = vecA + vecB;
    vecC += vecA;
    vecC  = vecA - vecB;
    vecC -= vecA;
    int val = 10;
    // ベクトルとスカラの乗除算
    vecC  = vecA * val;
    vecC *= val;
    vecC  = vecA / val;
    vecC /= val;
    // 外積
    vecC = vecA * vecB;
    // 内積
    double inner = vecA % vecB;
    // ノルム
    double norm = vecA.norm();
    // 正規化
    vecC = vecA.normalize();
    return 0;
}

行列の計算

次は行列の計算!

#include "arith/src/arith.h"
using namespace arith;
int main
{
    // 初期化
    Matrix matA(3, 2);    // 3行2列行列
    Matrix matB(3, 2);    // 3行2列行列
    // 要素の設定
    matA(0, 1) = 10;
    matB(2, 1) = 20;
    // 要素の取得
    double a01 = matA(0, 1);
    double b21 = matB(2, 1);
    // 行列同士の加減算
    Matrix matC = matA + matB;
    matC += matA;
    matC  = matA - matB;
    matC -= matB;
    Matrix matD(2, 3);
    // 行列同士の乗算
    Matrix matE = matD * matA;
    int val = 10;
    // 行列とスカラの乗除算
    matA  = matB * val;
    matA *= val;
    matA  = matB / val;
    matA /= val;
    // 転置行列
    matA = matD.transpose();
    return 0;
}

正方行列の計算

次は行列の中でも、行と列の数が同じ正方行列の計算です。正方行列にのみ定義されている値を計算できます。

#include "arith/src/arith.h"
using namespace arith;
int main
{
    // 初期化
    SquareMatrix smatA(3);    // 3行3列の正方行列
    Matrix matA(2, 2);
    // 正方行列への型変換
    SquareMatrix smatB = matA;
    // 行列式
    double det = smatA.det();
    // 逆行列
    if (det != 0) {
        SquareMatrix smatC = smatA.invrs(det);
    }
    return 0;
}

IonicによるAndroidアプリ作成の最低限

このページでは、UIフレームワークのIonicを使ったアプリの作成について紹介します。

Ionicのインストール

Ionicとその依存モジュールであるcordovaをインストールします。

$ npm install -g cordova ionic

アプリの新規作成

アプリ名はIonicAppとします。テンプレートは作りたいアプリに近いものを選ぶと楽です。

$ ionic start IonicApp blank

ionic start

実装

実装は作りたいアプリに応じてご自由に!!笑

デバッグ&実行

アプリ内のHTMLやJavascriptなどをPCでデバッグしたい場合、以下のコマンドを実行します。ファイルを更新すると自動でブラウザが更新されるのでとっても便利です。

$ ionic serve

モバイル機器でアプリを実行したい場合、対応するプラットフォームを追加します。iPhoneやiPadで実行したい場合、「android」ではなく「ios」となります。

$ ionic platform add android

追加したプラットフォームを実行します。

$ ionic run android

また、上記PCでの実行と同様に、HTMLやJavascriptを更新した時に、再ビルドなしで実行するためには、以下のコマンドを実行します。「-l」はlivereloadの意味です。このコマンドで実行した場合、HTMLなどのソースはモバイル機器にダウンロードされるのではなく、PCがWebサーバーとなってモバイル機器からアクセスされるようになります。

$ ionic run android -l

アイコンの作成

Ionicのアプリアイコンのファイルはresourcesフォルダ内にあります。icon.pngはホーム画面に表示されるアプリのアイコン画像であり、splash.pngはアプリ起動時に表示されるスプラッシュ画像です。これらのファイルをそれぞれ上書きしてから下記コマンドを実行すると、各プラットフォーム用のアイコンサイズに変換してくれます。

$ ionic resources

APK作成

作ったアプリをAndroidアプリとしてGoogle Playに公開するために、APKファイルを作成します。手順はPhoneGapアプリの公開用APKを作るをご覧ください。

多言語化

アプリが日本語で公開できるレベルになったら、ぜひ英語でも公開しましょう。一度仕組みを作ってしまえば簡単に多言語化できます。
まず多言語化を行うangular-translateと多言語ファイルを外部ファイルとして読み込むangular-translate-loader-static-filesをダウンロードします。

$ ionic add angular-translate
$ ionic add angular-translate-loader-static-files

これらのモジュールをHTMLから読み込みます。

<!-- www/index.html -->
<!DOCTYPE html>
  ...
  <script src="lib/angular-translate/angular-translate.min.js"></script>
  <script src="lib/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js"></script>
  ...
</html>

アプリのAngularJSモジュールとして追加します。

// www/js/app.js
angular.module('starter', [
 ... , 'pascalprecht.translate'
])
...

多言語ファイルの読み込み設定と言語の適用を行います。言語の適用はOSに設定された言語を使用し、もしその言語用のファイルがなければ変数defaultLangに入れた言語が適用されます。多言語ファイルはjs/langフォルダ内の言語コード.json(例:ja.json)を使用します。これで多言語化の準備は完了です。対応する言語を増やしたいときは、その言語の用語ファイルを追加するだけです。

// www/js/app.js
...
.config(function($translateProvider) {
  $translateProvider.useStaticFilesLoader({
    prefix: 'js/lang/',
    suffix: '.json'
  });
  var defaultLang = 'en';
  var getLang = function() {
    try {
      return (navigator.browserLanguage || navigator.language || navigator.userLanguage).substr(0, 2)
    } catch (e) {
      return defaultLang;
    }
  }
  $translateProvider.preferredLanguage(getLang());
  $translateProvider.fallbackLanguage(defaultLang);
})

実際に多言語化するためには、下記のような用語ファイルをlangフォルダに作ります。各メッセージにIDを振ります。

www/js/lang/ja.json
{
  "START": "はじめる",
  "GAMEOVER": "Game Over",
  "RESTART": "もう一度",
  "": ""
}
www/js/lang/en.json
{
 "START": "Start",
 "GAMEOVER": "Game Over",
 "RESTART": "Restart",
 "": ""
}

HTMLからこれらの用語を使うには、translate属性を追加してメッセージIDを指定します。

<!-- www/index.html -->
...
   <button class="button button-assertive" translate>START</button>
...

Javascriptから使うにはtranslateサービスを使います。

// controller.js
app.controller('Ctrl', ['$scope', '$translate', function($scope, $translate) {
  $scope.start = $translate('START');
}]);

終わりに

アプリ作成のために必要最低限のことを突っ走って書いてきましたが、他に必要だと思ったら適宜追記します。
僕自身はIonicを使って2つのアプリを作りました。動作の確認のためなど試しに触って見てください。
ぽりごん           Composition

kizAPIを使って関連語を取得する

このページでは、kizAPIを使ってある言葉の関連語を取得する方法を紹介します。

kizAPIとは

kizAPIとは、kizasi.jpというブログ検索サイトが提供しているWeb APIです。機能としては、ある言葉に関連する言葉を取得したり、今話題になっている言葉を取得できたりします。利用規約はここの最下部「利用規約」にあるので、利用する場合はあらかじめご確認ください。

関連語の取得

下記index.htmlをブラウザで表示すると、「sho」に関連する言葉が表示されます。依存モジュールとしてjQueryとjquery.xdomainajax.jsを利用しています。jquery.xdomainajax.jsはクロスドメイン対策で利用しています。ここからダウンロードして下さい。

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>kizAPI</title>
</head>
<body>
  <h1>Terms</h1>
  <div id="terms"></div>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  <script src="js/jquery.xdomainajax.js"></script>
  <script src="js/kizapi.js"></script>
  <script src="js/index.js"></script>
</body>
</html>

kizAPIにアクセスしているのがkizapi.jsです。引数で渡した検索語の関連語を取得できます。

// js/kizapi.js
/**
 * @fileOverview kizapi API
 */
(function(global) {
  'use strict';
  var kizapi = {
    /**
     * 関連語を取得する
     * @param    {string}   key   検索語&nbsp;
     * @param    {string}   span  '24':1日, '1w':1週間 or '1m':1月
     * @return   {object}         レスポンス
     * @property {string[]} terms 関連語
     */
    getRelatedTerms: function(key, span) {
      var defer = $.Deferred();
      if (this._reqFlag && this._reqId.abort) {
        this._reqId.abort();
      }
      this._reqFlag = true;
      var that = this;
      this._reqId = $.ajax({
        type: 'GET',
        url: 'http://kizasi.jp/kizapi.py',
        data: {
          span: span,
          kw_expr: key,
          type: 'coll'
        }
      }).done(function(data, statusText, jqXHR) {
        var xml = data.results[0];
        var json = $.parseXML(xml);
        var terms = [];
        $(xml).find('channel').find('item').each(function() {
          var item = $(this).text();
          var items = item.split(/\r?\n/g);
          var term = items[1].trim();
          terms.push(term);
        });
        defer.resolve({
          terms: terms
        });
      }).always(function() {
        that._reqFlag = false;
      });
      return defer.promise();
    },
    _reqId: {},
    _reqFlag: false
  };
  if ('process' in global) {
    module.exports = kizapi;
  }
  global.kizapi = kizapi;
}((this || 0).self || global));

kizAPIを呼び出しているのがindex.jsです。検索語「sho」の関連語を画面に表示しています。

// js/index.js
$(function() {
  var key = 'sho';
  kizapi.getRelatedTerms(key, '1m').done(function(data) {
   var html = '';
   var terms = data.terms;
   for (var i = 0, len = terms.length; i < len; i++) {
     html += '<span>' + terms[i] + '</span>;';
   }
   $('#terms').html(html);
 });
});

関連語取得サービス

上記のモジュールを利用して作ったのが「ke」です。「ke」は検索したいキーワードの関連語を表示してくれます。
ke

感想

以上、いろいろなWebAPIを試してみて、何に活用できるかを考えるのは楽しいですね。

KarmaでDOM操作のテストをする

このページでは、KarmaでDOM操作のテストをする方法を紹介します。

テスト対象

例として、下記のコードをテストしたいとします。index.htmlを開くと、メッセージが表示されるものです。

<!-- index.html -->
<!DOCTYPE html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <p id="message"></p>
  <script src="js/jquery.js">
  <script src="js/app.js">
  <script>
    $(function() {
      app.showMessage('init');
    }());
  </script>
</body>
</html>
// app.js
(function(global) {
  'use strict';
  global.app = {
    showMessage: function(message) {
      $('#message').text(message);
    }
  };
}(this.self));

フォルダ構成は下記の通りです。appフォルダにテスト対象のコードを入れ、specフォルダにテストコードを書いていきます。

Project
  ├─ app
  │    ├─ index.html
  │    └─ js
  │       ├─ jquery.js
  │       └─ app.js
  └─ spec

テスト環境構築

karma-html2js-preprocessorのインストール

karma-html2js-preprocessorはKarmaのプラグインであり、HTMLファイルをjsから読み込むことができるようになります。

$ npm install karma-html2js-preprocessor --save-dev

karmaの設定ファイルを変更し、filesとpreprocessorsにHTMLファイルを追加します。

// karma.conf.js
module.exports = function(config) {
  config.set({
    ...
    files: [
      'app/js/jquery.js',
      'app/js/app.js',
      'app/*.html'
      'spec/**/*.js',
    ],
    preprocessors: {
      'app/*.html': ['html2js']
    },
    ...
  });
};

テストコードの記述

specフォルダにテストコードappSpec.jsを作成し、beforeEachでdocumentのbodyにindex.htmlを読み込みます。テスト対象のコードでjQueryを使用しているので、テストコードからも使用しています。

// appSpec.js
describe('app', function() {
  var text = 'text';
  beforeEach(function() {
    document.body.innerHTML = __html__['app/index.html'];
  });
  it('should show message', function() {
    app.showMessage(text);
    expect($('#message').text()).toEqual(text);
  });
});

画像読み込みエラー

index.htmlで画像ファイルを読み込んでいる場合、テスト実行時にWARNINGが表示されます。

<!-- index.html -->
<!DOCTYPE html>
<html>
  ...
  <img src="img/image.png">
  ...
</html>
$ npm test
...
WARN [web-server]: 404: /img/image.png
...

このエラーが表示されないようにするには、プロキシを介して画像ファイルを読めるようにします。画像ファイルはファイルの変更監視が必要ないのと、ブラウザ起動時にscriptタグで読み込まないので、watchとincludedをfalseにします。プロキシには、ベースとなるフォルダのパスを基点にするために’base’を付加したパスを設定します。

// karma.conf.js
module.exports = function(config) {
  ...
  files: [
    'app/js/jquery.js',
    'app/js/app.js',
    'app/*.html',
    {pattern: 'app/**/*.png', watched: false, included: false, served: true},
    'spec/**/*.js'
  ],
  proxies: {
    '/img/': '/base/img/'
  },
  ...
};

Karmaでカバレッジを取る

このページでは、Karmaのテスト実行時にカバレッジを取る方法を紹介します。

カバレッジ取得環境の構築

karma-coverageのインストール

karma-coverageはkarmaのプラグインであり、テストを実行するとカバレッジが取れます。karmaのテスト環境構築方法はこちらで紹介しています。

$ npm install karma-coverage --save-dev

karmaの設定ファイルを修正します。reportersに’coverage’を追加し、preprocessorsにカバレッジを取りたいファイルを指定します。

// karma.conf.js
module.exports = function(config) {
  config.set({
    ...
    files: [
      'app/js/jquery.js',
      'app/js/app.js',
      'test/**/*.js'
    ],
    reporters: ['progress', 'coverage'],
    preprocessors: {
      'app/js/app.js': ['coverage']
    }
    ...
  });
};

カバレッジ取得

Karmaのテストを実行すると、自動的にカバレッジが取得します。取得したカバレッジはcoverageフォルダにHTML形式で保存されます。

$ npm test
(karma startのことです)

coverage/index.html
karma-coverage
 

ちなみに

カバレッジデータはテストを実行すれば作成されるため、コミットする必要はありません。バージョン管理している場合はignoreに追加しておきましょう。

[.gitignore]
...
coverage