ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【Raspberry Pi】SDカードのバックアップ(コピー)をWindowsで取る方法

 以前、MacOSにおけるSDカードのバックアップ(コピー)の記事を書きました。

 Windowsの場合はコマンドでSDカードのコピーはできないので、今回はWindowsでSDカードのコピーを行う方法の備忘録になります。

 では始めます。


1:Win32 Disk Imagerのインストール
 以下のページにある「win32diskimager-1.0.0-install.exe」をダウンロードします。

 ダウンロードしたインストーラを実行して指示通りにしていけば、Win32 Disk Imagerをインストールできます。


2:Win32 Disk Imagerを使ってSDカードをコピーする
 SDカードをコピーする前に空のイメージファイルを作成しておく必要があります。

 場所はどこでもいいのですが例えばデスクトップに以下のように新規テキストファイルを作成します。
f:id:rikoubou:20201217153921p:plain

 作成したテキストファイルのファイル名はなんでもよいですが、拡張子の部分を「.img」に変更します。
f:id:rikoubou:20201217154333p:plainf:id:rikoubou:20201217154336p:plain

 これで空のイメージファイルができたので、このファイルにSDカードの内容をコピーしていきます。

 コピーしたいSDカードをPCに認識させた状態でWin32 Disk Imagerを起動させ、右上にある「Device」のプルダウンからコピーしたい対象のSDカードを選択します。
f:id:rikoubou:20201217154549p:plain

 SDカードを選択した後、その横にある「フォルダのアイコン」をクリックして先ほど作成した空のイメージファイルを指定します。
f:id:rikoubou:20201217154743p:plain

 ここまで準備ができたら「Read」ボタンをクリックします。するとSDカードのコピーが始まります。
f:id:rikoubou:20201217154934p:plain

 SDカードを丸ごとコピーするので、SDカードの容量分のイメージファイルが作成されます。つまりPC側にもそれ以上の容量をあらかじめ確保しておく必要があるので注意が必要です。

 PCのスペックやSDカードの容量によってコピー時間は変わってきますが、自分の環境だと32GBのSDカードのコピーで20分程度でした。


3:コピーしたSDカードのイメージファイルを書き込む
 イメージファイルを書き込む場合は基本的に2とほぼ同じです。

 書き込みたい新品のSDカードをPCに認識させて「Device」の項目で選択し、イメージファイルにコピーしたSDカードのものを指定します。そして「Write」ボタンをクリックすると書き込みが開始されます。
f:id:rikoubou:20201217155444p:plain

 こちらもPCのスペックやSDカードの容量によってコピー時間は変わってきますが、自分の環境だと32GBのSDカードイメージファイルの書き込みは20分程度でした。


 以上がSDカードのバックアップ(コピー)をWindowsで取る方法になります。

 MacoSの場合はコマンドでできて便利といえば便利でしたが、Windowsだとソフトを入れる必要があるのでちょっと面倒ではあります。
 ただしGUIで操作できるので、一度インストールしてしまえば簡単に使えて便利そうです。


・参考資料

【Raspberry Pi】BluetoothやWiFiを無効にする

 Raspberry Piは何かと便利で高性能ですが、その分消費電力はそれなりにあります。

 何も通信したりせずにスタンドアロンで動かすような場合は、BluetoothWiFiを切っておいた方が消費電力としては小さくなります。

 なので今回はそんなRaspberry PiBluetoothWiFiを無効にする方法の備忘録になります。

 ちなみにRaspberry Pi 4での方法なので他のRaspberry Pi 3等とやり方が違う可能性があるので注意してください。

 では始めます。


1:Bluetoothを無効にする
 やり方は簡単で「/boot/config.txt」ファイルに1行追記するだけです。

 今回はnanoですが、なんのエディタでも良いので「/boot/config.txt」ファイルを開きます。

$ sudo nano /boot/config.txt

「/boot/config.txt」を開いたら末尾に以下の記述を追記します。

dtoverlay=disable-bt

 追記できたら上書き保存します。

 保存できたらRaspberry Piを再起動させます。するとGUI画面右上に出ていたBluetoothのマークが表示されなくなりBluetoothが無効になっています。

 boot時のファイルに追記しているので、一度この設定を行えば起動時に自動的にBluetoothが無効になります。


2:WiFiを無効にする
 Bluetoothの無効化はboot時のファイルに記述しましたが、WiFiは時々使いたくなる場合もあるかと思うので今回はコマンドによってWiFiの有効/無効を切り替えます。

 WiFiを無効にするには「LXTerminal」を立ち上げて以下のコマンドを実行します。

WiFi無効

$ sudo iwconfig wlan0 txpower off

WiFi有効

$ sudo iwconfig wlan0 txpower auto

 これらはコマンドで実行するとすぐに反映されるため、再起動する必要はありません(ただしWiFi有効化時はWiFiに接続するまで数十秒かかります)。

 例えば起動時に自動的にWiFiを無効にしたい場合は、シェルファイルなどにコマンドを記述しておいて起動時にそのシェルを自動実行するようにしておいたりするのが良いかと思います。

 またこの他にもGUIの右上にあるWiFiのマークからの手動でWiFiの有効/無効を切り替えられます。


 以上がRaspberry PiBluetoothWiFiを無効にする方法です。

 案外使う場面があるかと思うので、消費電力を気にする場合は設定しておくとよいかもしれません。


・参考資料

【python】pythonを使ってGET/POST/PUTを行う

 今回はpythonを使ってGET/POST/PUTのそれぞれを行う方法です。

 また今回扱う方法はpython3系でのものになるので注意してください。

 では始めます。


1:GET/POST/PUTを行う記述
 それぞれ以下のように記述します。

・GET

import urllib.request

def url_get(url_str):
    try:
        res_code = None
        res_text = None
        # GET
        response = urllib.request.urlopen(url_str)
        res_code = response.getcode()
        html = response.read()
        res_text = html.decode('utf-8')
    except Exception as e :
        raise e
    finally:
        return res_code, res_text


・POST

import urllib.request
import json

def url_post(url_str, data_dict):
    try:
        res_code = None
        res_text = None
        # POST
        headers = {"Content-Type" : "application/json"} # json形式の場合必須
        data = json.dumps(data_dict).encode("utf-8")
        request = urllib.request.Request(url_str, data, method='POST', headers=headers)
        response = urllib.request.urlopen(request)
        res_code = response.getcode()
        html = response.read()
        res_text = html.decode('utf-8')
    except Exception as e :
        raise e
    finally:
        return res_code, res_text

・PUT

import urllib.request
import json

def url_put(url_str, data_dict):
    try:
        res_code = None
        res_text = None
        # PUT
        headers = {"Content-Type" : "application/json"} # json形式の場合必須
        data = json.dumps(data_dict).encode("utf-8")
        request = urllib.request.Request(url_str, data, method='PUT', headers=headers)
        response = urllib.request.urlopen(request)
        res_code = response.getcode()
        html = response.read()
        res_text = html.decode('utf-8')
    except Exception as e :
        raise e
    finally:
        return res_code, res_text

 読めば何となくわかるとは思いますが、GETの場合は「urllib.request.urlopen」関数を使って引数の結果を取得しています。存在しないURLなどエラーが発生した場合はExceptionがraiseされます(本当はtry exceptの範囲を狭めるべきだとは思いますが面倒なので全部を範囲にしてます…)。

 POSTとPUTの場合は「urllib.request.Request」関数でリクエストを作成してから「urllib.request.urlopen」関数を使っています。
 またこれら2つはヘッダーとして「{"Content-Type" : "application/json"}」を追加しており、これによりjson形式を送ることができます。json形式を送っているのにこのヘッダーを追加していなかった場合、jsonではなくjson文字列として送られてしまうので注意が必要です。


2:サンプルを動かす
 ではサンプルを動かしていきます。と言ってもPOSTとPUTは簡単に試せないので、今回はGETだけです。

・request_test.py

# -*- coding:utf-8 -*-
import urllib.request
import json

def main():
    get_code, get_text = url_get("https://www.google.com/")
    print(get_code)
    print(get_text)

    # POSTは適宜書き換えて試してください
    # post_data = {"test", "てすと"}
    # post_code, post_text = url_post("https://www.google.com/", post_data)
    # print(post_code)
    # print(post_text)

    # PUTは適宜書き換えて試してください
    # put_data = {"test", "てすと"}
    # put_code, put_text = url_put("https://www.google.com/", put_data)
    # print(put_code)
    # print(put_text)


def url_get(url_str):
    try:
        res_code = None
        res_text = None
        # GET
        response = urllib.request.urlopen(url_str)
        res_code = response.getcode()
        html = response.read()
        res_text = html.decode('utf-8')
    except Exception as e :
        raise e
    finally:
        return res_code, res_text


def url_post(url_str, data_dict):
    try:
        res_code = None
        res_text = None
        # POST
        headers = {"Content-Type" : "application/json"}
        data = json.dumps(data_dict).encode("utf-8")
        request = urllib.request.Request(url_str, data, method='POST', headers=headers)
        response = urllib.request.urlopen(request)
        res_code = response.getcode()
        html = response.read()
        res_text = html.decode('utf-8')
    except Exception as e :
        raise e
    finally:
        return res_code, res_text


def url_put(url_str, data_dict):
    try:
        res_code = None
        res_text = None
        # PUT
        headers = {"Content-Type" : "application/json"}
        data = json.dumps(data_dict).encode("utf-8")
        request = urllib.request.Request(url_str, data, method='PUT', headers=headers)
        response = urllib.request.urlopen(request)
        res_code = response.getcode()
        html = response.read()
        res_text = html.decode('utf-8')
    except Exception as e :
        raise e
    finally:
        return res_code, res_text


if __name__ == "__main__":
    main()


 以上がpythonを使ってGET/POST/PUTを行う方法です。

 pythonからAPIを呼び出したりする場合に使うことになると思います。

 またpython2系と3系両方で使える書き方もあり、その方法でGoogle検索の結果を取得している方もいるので色々と便利そうです。


・参考資料

【python】pythonを使ってGmailを自動送信する

 プログラムで異常が起こった時の通知メールや定期的な通知メールを行う際に、自動的にメールを送信する機能があると何かと便利です。

 今回はそのような場面で使えそうなpythonを使ってGmailを自動送信する方法の備忘録になります。

 では始めます。


1:Gmailのセキュリティ設定を低くする
 pythonを使ってGmailを送信するためには、Googleアカウントのセキュリティを「低く」する必要があります。なので通知専用のGmailアドレスを取得するなどするのが良いかと思います。

 Googleアカウントのセキュリティを低くするためには、まず通知したいGmailを持つGoogleアカウントにログインをして「Googleアカウント」画面を開きます。そして左側にある「セキュリティ」の項目をクリックし、スクロールさせた先にある「安全性の低いアプリのアクセス」を「オン」にします。
f:id:rikoubou:20201215173126p:plain

 これでpythonを使ってGmailを送信できるようになります。


2:pythonGmailを送信するサンプル
 では実際にGmailを送信してみます。ソースコード中の「適宜書き換える」と書いてある部分はそれぞれ自分用に書き換えてください。

gmail_test.py

# -*- coding:utf-8 -*-
import smtplib, ssl
from email.mime.text import MIMEText
from datetime import datetime


# アカウント情報
gmail_account  = "XXXXXX@gmail.com" # Gmailアカウントアドレス(適宜書き換える)
gmail_password = "xxxxxxxxxxx"      # パスワード(適宜書き換える)

# context作成
context = ssl.create_default_context()

# Gmailにログイン
server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context)
server.login(gmail_account, gmail_password)

# 本文(htmlなので改行は<br>を使う)
body_str = "test" + "<br>"
body_str += "てすとですよ" + "<br><br>"
body_str += datetime.now().strftime("%Y/%m/%d %H:%M:%S") # 現在時刻も追加

# メールデータ(MIME)作成
msg = MIMEText(body_str, "html")
msg["Subject"] = "メールタイトル"     # メールタイトル
msg["To"]      = "yyyyyyy@gmail.com" # 送信先メールアドレス(適宜書き換える)
msg["From"]    = gmail_account       # 送信元メールアドレス

# メール送信
server.send_message(msg)

 実行してエラーが出なければメールが送られているので確認してみてください。


 以上がpythonを使ってGmailを自動送信する方法です。

 Googleアカウントのセキュリティを下げる必要はありますが、上手く使えば何かと便利だと思います。


・参考資料

【python】jsonファイルの扱いについて

 pythonを使っていてjsonファイルを扱いたくなる時があります。

 扱うのは簡単なのですが、文字コードあたりと絡むと面倒だったり似たような名前の関数でどっちがどっちだっけ、みたいになって自分も混乱していたので記事にしようと思った次第です。

 今回はそのjsonファイルを扱う方法についての備忘録です。

 では、始めます。


1:jsonファイルを扱う関数について
 基本的には以下の4つの関数を使うだけです。

import json

json.load()  # jsonファイルを辞書に変換
json.dump()  # 辞書をjsonファイルに保存

json.loads() # json文字列を辞書に変換
json.dumps() # 辞書をjson文字列に変換

 具体的には以下のように使います。

json.load

# ファイルが存在していればjsonファイル読み込み
if os.path.exists(json_path):
    # windowsの場合は「encoding="utf-8_sig"」にする
    with open(json_path, "r", encoding="utf-8") as f:
        data = json.load(f) # jsonを辞書として読み込み

json.dump

# utf-8で書き込み
with open(save_path, 'w', encoding='utf-8') as f:
    # インデントをつけてアスキー文字列ではない形で保存
    json.dump(data, f, indent=4, ensure_ascii=False)

json.loads

# json文字列(str)を辞書に変換する関数
data = json.loads(data)

json.dumps

# 辞書をjson文字列に変換
data = json.dumps(data, indent=4, ensure_ascii=False)

 詳しい変換の方法は以下のページ様が詳しいので気になる方は参照してください。

 sがついてある方がjson文字列としての操作、sがついてない方がファイル操作の関数という風に覚えるといいかもしれません。

 これらは自分はごっちゃになってしまったので、それぞれの使い方の例を挙げていきます。


2:jsonファイルを扱うサンプル
 サンプルとして2つのファイルをコピペして同じ階層にutf-8の形式で保存して実行します。

json_test.json

# coding: utf-8
import os
import json


# 型と中身を表示させる関数
def print_data(data):
    print(type(data))
    print(data)


def main():
    json_path = "./test.json" # jsonファイルパス
    save_path = "./save.json" # 保存ファイルパス

    data = None

    # ファイルが存在していればjsonファイル読み込み
    if os.path.exists(json_path):
        # windowsの場合は「encoding="utf-8_sig"」にする
        with open(json_path, "r", encoding="utf-8") as f:
            data = json.load(f) # jsonを辞書として読み込み
    print_data(data) # 内容確認

    # 中身を一部書き換え
    test1_data = data['test1']
    test1_data['number'] = 3
    test1_data['text'] = "書き換え"

    # 辞書をjson文字列に変換
    data = json.dumps(data, indent=4, ensure_ascii=False)
    print_data(data) # 内容確認

    # json文字列(str)を辞書に変換する関数
    data = json.loads(data)
    print_data(data) # 内容確認

    # utf-8で書き込み
    with open(save_path, 'w', encoding='utf-8') as f:
        # インデントをつけてアスキー文字列ではない形で保存
        json.dump(data, f, indent=4, ensure_ascii=False)


if __name__ == '__main__':
    main()

・test.json

{
    "test1"  : {"number" : 1, "text": "てすと1"},
    "test2"  : {"number" : 2, "text": "てすと2"}
 }

 実際に実行すると以下のような結果になります。

$ python json_test.py
<class 'dict'>
{'test1': {'number': 1, 'text': 'てすと1'}, 'test2': {'number': 2, 'text': 'てすと2'}}
<class 'str'>
{
    "test1": {
        "number": 3,
        "text": "書き換え"
    },
    "test2": {
        "number": 2,
        "text": "てすと2"
    }
}
<class 'dict'>
{'test1': {'number': 3, 'text': '書き換え'}, 'test2': {'number': 2, 'text': 'てすと2'}}

 新規作成された「save.json」ファイルを開くと、ちゃんと書き換えられた内容で保存されていることも確認できます。

・save.json

{
    "test1": {
        "number": 3,
        "text": "書き換え"
    },
    "test2": {
        "number": 2,
        "text": "てすと2"
    }
}


 以上がpythonにおけるjsonファイルの扱い方です。

 文字コードが絡んで一気に面倒になってしまった経験があるので、同じように困っている人のお役に立てば幸いです。


・参考資料

【python】日時を扱うdatetimeについて

 pythonで日時の計算をしたい場合があります。

 その場合は標準ライブラリであるdatetimeを使うことになりますが、毎回調べたりするのが面倒なのでその備忘録として記事にした次第です。


 では、始めます。


1:datetimeで現在日時を取得する
 まずはよくある使い方として現在日時を取得する方法です。

 以下のように記述するだけで現在時刻を取得できます。

from datetime import datetime

now_datetime = datetime.now() # 現在日時を取得

print(now_datetime)

# 結果例
# 2020-12-11 18:53:59.833597

 いい感じの文字列として整形したい場合は以下のようにstrftimeを使います。

from datetime import datetime

now_datetime = datetime.now() # 現在日時を取得
now_datetime_str = now_datetime.strftime("%Y/%m/%d %H:%M:%S") # 現在日時を整形(yyyy/mm/dd HH:MM:SS)

print(now_datetime_str)

# 結果例
# 2020/12/11 18:57:39


2:文字列で来た日時をdatetimeに変換する
 DBなどに保存された文字列の日時情報をdatetimeに変換して扱いたい場合もあるかと思います。

 その場合は以下のようにします。

from datetime import datetime

dt = datetime.strptime("2020/12/10 13:02:37", "%Y/%m/%d %H:%M:%S") # 任意のdatetime作成

print(type(dt))
print(dt)
# 結果
# <class 'datetime.datetime'>
# 2020-12-10 13:02:37

 その他にも以下のような特殊な場合は余計な部分を削除したり置換したりして整形します。

from datetime import datetime

# ちょっと特殊な場合
test_date = "2020/12/11T10:07:45.973831+09:00"

# 変換に邪魔になる部分を置換
test_date = test_date.replace("T"," ")
test_date = test_date.replace("+09:00","")

# 日時を整形
test_date = datetime.strptime(test_date, "%Y/%m/%d %H:%M:%S.%f")

print(type(test_date))
print(test_date)

# 結果
# <class 'datetime.datetime'>
# 2020-12-11 10:07:45.973831


3:datetimeの差分を計算する
 日時同士の差分を取って時間経過を知りたい場合もあるかと思います。

 その場合は以下のようにします。ちなみに過去日時から未来日時を引いた場合、マイナスの日時になります。

from datetime import datetime

# 過去の時間
time_1 = datetime.strptime("2020/12/10 13:02:37", "%Y/%m/%d %H:%M:%S")
print(time_1)

# 少し先の時間
time_2 = datetime.strptime("2020/12/10 13:12:37", "%Y/%m/%d %H:%M:%S")
print(time_2)

# 差分を計算
result = time_2 - time_1
print("time_2 - time_1 = {}".format(str(result)))

# 差分秒数を取得
result_seconds = result.total_seconds()
print(result_seconds)

if result_seconds >= 600.0:
    # 10分(600秒以上)差分があるかを判定
    print("10 minute over!")

# ----- 過去日時から未来日時を引くとマイナスになる -----
result2 = time_1 - time_2
print("time_1 - time_2 = {}".format(str(result2)))

# 差分秒数を取得
result_seconds2 = result2.total_seconds()
print(result_seconds2)


 以上がpythonにおけるdatetimeの扱い方です。

 日時の差分を取ったりするのは何かと使う機会があるかと思います。


・参考資料

【python】unittestでmockを使ってテストケースを書く

 以前の記事でテストケースを書く方法を紹介しました。

 簡単なプログラムであれば上記の内容だけである程度テストはできると思いますが、他のクラスからの呼び出しだったり、対象の関数が何回呼ばれているかを確かめたり、まだ実装してないクラスを呼び出していたりという場合には対応できません。

 そういう時に便利なのがモックと言われるもので、要はダミーのクラスや関数を設定して戻り値を固定させたり呼び出し回数をカウントしたりするためのものです。

 今回はpythonに最初から含まれているunittestを使ってそんなモックを使ったテストケースを書いた時の備忘録です。


 では、始めます。


1:unittestにおけるモックの書き方
 モックの書き方を簡単にですが説明していきます。基本的には以下の部分がわかっていれば大体の場合で問題はないかと思います。

・呼び出す関数をモックにしたい場合

# -*- coding:utf-8 -*-
from unittest import (TestCase, skip)
from unittest import mock

from hoge import Hoge # テスト対象ファイル読み込み

class TestTarget(TestCase):
    def _mock_function(self):
        # モックとしての処理を書く

    @mock.patch("Hoge.function", new=_mock_function)
    def test_hoge(self):
        hoge = Hoge()
        hoge.function() # ここの関数で_mock_functionが呼ばれる

if __name__ == '__main__':
    unittest.main()

 テストを行う関数test_hogeの前にある「@mock.patch("Hoge.function", new=_mock_function)」という記述でHogeクラスのfunction関数を自分で定義したモック関数である「_mock_function」に置き換えています。ちなみにこの書き方だとモック関数のselfはモック先のインスタンスになるので注意が必要です。

・呼び出すクラス自体をモックにしたい場合

# -*- coding:utf-8 -*-
from unittest import (TestCase, skip)
from unittest import mock

from hoge import Hoge # テスト対象ファイル読み込み

# 例えば以下のようにHogeの内部で
# 別のクラスのインスタンスを持っている場合
# class Hoge():
#     def __init__(self):
#         self.hogehoge = HogeHoge()

class TestTarget(TestCase):
    def test_hoge(self):
        hoge = Hoge()

        # hogeが持っているhogehogeインスタンスをモック用インスタンスに置き換え
        hoge.hogehoge = mock.MagicMock()

        # モックインスタンスが呼ばれているので何も行われない
        hoge.hogehoge.function()

        # モックインスタンスでfunction関数が呼ばれた回数を確認
        self.assertEqual(1, hoge.hogehoge.function.call_count)


if __name__ == '__main__':
    unittest.main()

 モックにしたいHogeHogeクラスをmock.MagicMock()というクラスで置き換えています。これはモック用のクラスで関数が呼ばれた回数を数えたりすることができる便利なクラスです。詳しい内容は参考資料に挙げているページ様や公式ドキュメントを参照してください。


2:実際にテストするコードの準備
 書き方だけ説明されてもどう使えばいいのかわからないと思うので、実際にモックを使ったテストコードを書いていきます。

 今回は以下のコードをテストする前提で進めていきます。そのまま使いたい場合はコピペして2つのファイルを同じ階層に保存してください。

・target.py

# -*- coding:utf-8 -*-
#
# target.py
#
from sumClass import SumClass

class Target():
    # コンストラクタ
    def __init__(self):
        self.sum1 = SumClass()
        self.sum2 = SumClass()

    # 初期化関数
    def init(self, sum1_add, sum1_loop, sum2_add, sum2_loop):
        # sum1を設定
        self.sum1.setAdd(sum1_add)
        self.sum1.setLoopTimes(sum1_loop)
        # sum2を設定
        self.sum2.setAdd(sum2_add)
        self.sum2.setLoopTimes(sum2_loop)

    # main関数
    def main(self):
        self.sum1.sumLoop()
        self.sum2.sumLoop()
        result_str = self.sumResultsToString()
        print(result_str)

    # 結果を文字列にする関数
    def sumResultsToString(self):
        result_str = "[result] sum1:{}, sum2:{}"
        return result_str.format(int(self.sum1.result), int(self.sum2.result))

if __name__ == "__main__":
    target = Target()
    target.init(3, 5, 2, 10)
    target.main()

・sumClass.py

# -*- coding:utf-8 -*-
#
# sumClass.py
#
class SumClass():
    # コンストラクタ
    def __init__(self):
        self.result = 0
        self.add = 0
        self.loop_times = 0

    # 足す数を設定
    def setAdd(self, add):
        self.add = add

    # ループ回数を設定
    def setLoopTimes(self, loopTimes):
        self.loop_times = loopTimes

    # 足し算を繰り返す関数
    def sumLoop(self):
        count = 1
        while count <= self.loop_times:
            self.result = self.mySum(self.result, self.add)
            count += 1

    # 足し算を行う関数
    def mySum(self, add1, add2):
        return add1 + add2

 今回はこの2つのファイルのうちの「target.py」のテストコードを書いていきます。


3:テストコードの実装
 2つのファイルと同じ階層に「tests」フォルダを作成し、その中に「test_target.py」ファイルを作成します。
f:id:rikoubou:20201126172647p:plain
f:id:rikoubou:20201126172723p:plain

・test_target.py

# -*- coding:utf-8 -*-
# target.pyファイルがある階層で以下のコマンドでテストを実行する
# python -m unittest tests.test_target

from unittest import (TestCase, skip)
from unittest import mock

from sumClass import SumClass
from target import Target # テスト対象ファイル読み込み

# モック用クラス
class MockSumClass(mock.MagicMock):
    init_count = 0
    result_count = 0

    # SumClassのコンストラクタモック用
    def _mock_SumClass_init(self):
        self.init_count += 1

    # sumResultsToString関数のモック用
    def _mock_sumResultsToString(self):
        self.result_count += 1


class TestTarget(TestCase):
    _mock_sum = MockSumClass() # モッククラスのインスタンス

    # コンストラクタのテスト
    @mock.patch("sumClass.SumClass.__init__", new=_mock_sum._mock_SumClass_init)
    def test_01__init__(self):
        Target() # 実行

        # SumClassのコンストラクタが呼ばれた回数を確認
        self.assertEqual(2, self._mock_sum.init_count)


    # init関数のテスト
    def test_02_init(self):
        target = Target()

        # モッククラスに置き換え
        target.sum1 = mock.MagicMock()
        target.sum2 = mock.MagicMock()

        target.init(1, 2, 3, 4) # 実行

        # 呼び出し回数の確認
        self.assertEqual(1, target.sum1.setAdd.call_count)
        self.assertEqual(1, target.sum1.setLoopTimes.call_count)
        self.assertEqual(1, target.sum2.setAdd.call_count)
        self.assertEqual(1, target.sum2.setLoopTimes.call_count)


    # main関数のテスト(全てモックの場合)
    @mock.patch("target.Target.sumResultsToString", new=_mock_sum._mock_sumResultsToString)
    def test_03_main_01(self):
        self._mock_sum.result_count = 0
        target = Target()

        # モッククラスに置き換え
        target.sum1 = mock.MagicMock()
        target.sum2 = mock.MagicMock()

        target.main() # 実行

        # 呼び出し回数の確認
        self.assertEqual(1, target.sum1.sumLoop.call_count)
        self.assertEqual(1, target.sum2.sumLoop.call_count)
        self.assertEqual(1, self._mock_sum.result_count)


    # main関数のテスト(計算結果を見る場合)
    @mock.patch("target.Target.sumResultsToString", new=_mock_sum._mock_sumResultsToString)
    def test_04_main_02(self):
        self._mock_sum.result_count = 0
        target = Target()

        target.init(1, 2, 3, 4)
        target.main() # 実行

        # 計算結果の確認
        self.assertEqual(2, target.sum1.result)
        self.assertEqual(12, target.sum2.result)
        # 呼び出し回数の確認
        self.assertEqual(1, self._mock_sum.result_count)


    # sumResultsToString関数のテスト
    def test_05_sumResultsToString_01(self):
        target = Target()

        result = target.sumResultsToString() # 実行

        # 確認
        self.assertEqual("[result] sum1:0, sum2:0", result)


    # sumResultsToString関数のテスト2
    def test_06_sumResultsToString_02(self):
        target = Target()
        target.init(1, 2, 3, 4)
        target.main()

        result = target.sumResultsToString() # 実行

        # 確認
        self.assertEqual("[result] sum1:2, sum2:12", result)


if __name__ == '__main__':
    unittest.main()

 テストコードの実行は「target.py」ファイルと同じ階層で以下のコマンドで実行できます。

$ python -m unittest tests.test_target

..None
.None
..[result] sum1:2, sum2:12
.
----------------------------------------------------------------------
Ran 6 tests in 0.006s

OK

 あまり良いテストコードではない気がしますが、それぞれ呼び出し回数やモック関数の呼び出しがちゃんとできています。

 また、個人的にはモック用のクラスを作成してそれを呼び出すという形にした方が呼び出し回数など色々制御しやすいので楽なのではないかと思いそういう形にしています。


 以上がunittestでmockを使う方法です。

 正直まだ自分自身もmockに慣れていないので、もうちょっと良い書き方等があればコメントしていただけると幸いです。


・参考資料