DjangoでCoverage.pyを使ってカバレッジを計測する

2017年5月10日

django-nosedjango-caverageを使ってテストコードの実行とカバレッジの計測を行っていましたが、django 1.10ではうまくカバレッジを計測してくれないという問題があったので、今回はCoverage.pyを利用してカバレッジを計測してみたいと思います。

django-nosedjango-caverageを利用したテストコードの実行方法を知りたい方は、過去の記事を参照してください。

Djangoでテストコードを書く

django-noseとdjango-caverageを利用していた時の問題点

Django 1.10でカバレッジを計測していた際に、下記のコードのカバレッジが計測できない問題がありました。

  • デコレータのカバレッジが計測されない
  • 多重継承しているスーパークラスのカバレッジが計測されない

デコレータのカバレッジが計測されないために、全てのコードをパスしているテストコードを書いてもカバレッジが100%にならず、計測結果をみてもこれ以上テストが必要かどうかをカバレッジから確認することができない状態でした。
そこで、Coverage.pyを利用してカバレッジを計測するようにしました。

Coverage.pyをインストールする

pipを使ってCoverage.pyをインストールします。

$ pip install coverage

Coverage.pyの設定ファイルを作成する

プロジェクトディレクトリ直下にCoverage.pyに関する設定を記述する「.coveragerc」というファイルを作成します。
とりあえず、最低限の設定を記述しておきます。

[run]
omit = */tests/*

[html]
directory = cover

.coveragercの設定に関する詳細な情報については下記のサイトを参照してください。

テストコードを準備する

今回は「accounts」アプリケーションのViewModelをテストを実施します。
下記のテストを実施するテストコードになります。

  • Viewのテスト
    • ログイン画面へのアクセステスト
    • ログイン成功時のテスト
    • ログイン失敗時のテスト
  • Modelのテスト
    • ユーザ作成(create_user)のテスト
    • スーパーユーザ作成(create_superuser)のテスト
# accounts/tests.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible
from django.test import TestCase
from .models import AuthUser


@python_2_unicode_compatible
class AccountsViewTests(TestCase):
    def setUp(self):
        """
        ログイン可能なユーザを1名追加する
        """
        AuthUser.objects.create_user(username='test',
                                     email='test@test.com',
                                     password='test',
                                     last_name='テスト',
                                     first_name='太郎')

    def test_login_get(self):
        """
        ログイン画面へのアクセスのテスト
        """
        client = self.client

        response = client.get('/accounts/login/')
        self.assertEqual(response.status_code, 200)

        client.login(username='test', password='test')

        response = client.get('/accounts/login/')
        self.assertEqual(response.status_code, 200)

    def test_login_success(self):
        """
        ログイン画面でログインできるかテストする
        """
        client = self.client

        response = client.post('/accounts/login/', data={'username': 'test', 'password': 'test'})
        self.assertRedirects(response, '/')

    def test_login_fail(self):
        """
        ユーザ登録されている人以外はログインできないことをテストする
        """
        client = self.client

        response = client.post('/accounts/login/', data={'username': 'test2', 'password': 'test'}, follow=True)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.request['PATH_INFO'], '/accounts/login/')


@python_2_unicode_compatible
class AccountsModelTests(TestCase):
    def test_create_user(self):
        """
        AuthUserモデルのcreate_userをテストする
        """
        # 普通にユーザ追加
        result = AuthUser.objects.create_user(username='test', email='test@test.com', password='test', last_name='テスト', first_name='太郎')

        user = AuthUser.objects.get(pk=result.pk)
        # ちゃんとパラメータにセットした値でユーザが登録されたか確認する
        self.assertEqual(result.username, 'test')
        self.assertEqual(result.email, 'test@test.com')
        # ハッシュ値は常にランダムになってしまうので、ここはDBに登録された値と比較する
        self.assertEqual(result.password, user.password)
        self.assertEqual(result.last_name, 'テスト')
        self.assertEqual(result.first_name, '太郎')
        # 以下の3つのフィールドはcreate_userした時に自動でセットされる項目なので、DBのレコードの値を検証する
        self.assertTrue(user.is_active)
        self.assertFalse(user.is_staff)
        self.assertFalse(user.is_superuser)

        # eメールが空だったらエラーをはく
        # エラー時のメッセージも検証する
        with self.assertRaisesMessage(ValueError, 'Users must have an email'):
            AuthUser.objects.create_user(username='test', email='', password='test', last_name='テスト', first_name='太郎')

        # usernameが空だったらエラーをはく
        # エラー時のメッセージも検証する
        with self.assertRaisesMessage(ValueError, 'Users must have an username'):
            AuthUser.objects.create_user(username='', email='test@test.com', password='test', last_name='テスト', first_name='太郎')

    def test_create_superuser(self):
        """
        AuthUserモデルのcreate_superuserをテストする
        """
        # スーパーユーザを追加する
        result = AuthUser.objects.create_superuser(username='super', email='super@user.com', password='user', last_name='スーパー', first_name='ユーザ')

        user = AuthUser.objects.get(pk=result.pk)
        # ちゃんとパラメータにセットした値でユーザが登録されたか確認する
        self.assertEqual(result.username, 'super')
        self.assertEqual(result.email, 'super@user.com')
        # ハッシュ値は常にランダムになってしまうので、ここはDBに登録された値と比較する
        self.assertEqual(result.password, user.password)
        self.assertEqual(result.last_name, 'スーパー')
        self.assertEqual(result.first_name, 'ユーザ')
        # 以下の3つのフィールドはcreate_userした時に自動でセットされる項目なので、DBのレコードの値を検証する
        self.assertTrue(user.is_active)
        self.assertTrue(user.is_staff)
        self.assertTrue(user.is_superuser)

        # eメールが空だったらエラーをはく
        # エラー時のメッセージも検証する
        with self.assertRaisesMessage(ValueError, 'Users must have an email'):
            AuthUser.objects.create_superuser(username='super', email='', password='user', last_name='スーパー', first_name='ユーザ')

        # usernameが空だったらエラーをはく
        # エラー時のメッセージも検証する
        with self.assertRaisesMessage(ValueError, 'Users must have an username'):
            AuthUser.objects.create_superuser(username='', email='super@user.com', password='user', last_name='スーパー', first_name='ユーザ')

テストコードを実行してカバレッジをHTMLで出力する

下記コマンドを実行して、テストコードを実行しカバレッジをHTMLファイルで出力します。

$ coverage run --source=accounts manage.py test accounts
$ coverage report
$ coverage html

上記実行すると.coveragerc[html]で設定したcoverディレクトリにカバレッジの計測結果が格納されます。

計測されたカバレッジを確認する

coverディレクトリのindex.htmlをブラウザで開くとカバレッジの計測結果が確認できます。

accounts/models.pyのカバレッジの詳細を確認してみます。

デコレータやスーパークラスのコードもカバレッジが通っていることが確認できました!!

最後に

django-noseもカバレッジを簡単に計測することができて、使い方も簡単なのですが、「Django 1.8」以降を利用して開発している場合、django-noseDjangoの新機能に対応しておらず、カバレッジが正確に測ることができなくなってしまっていました。
もし、Django 1.8以降のバージョンを利用して開発を行っている方で、カバレッジをうまく計測できずにお悩みの方はCoverage.pyに切り替えるのもアリだと思います。