Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

BDD(振る舞い駆動開発)

BDD(Behavior-Driven Development、振る舞い駆動開発) は「システムが利用者や業務の観点でどのように振る舞うべきか」を具体例として定義しながら開発する手法。

TDDが「コード単位のテスト」から設計を進める傾向が強いのに対し、BDDは「利用者から見た振る舞い」を中心に考える。

BDDの種類

Behatの制作者であるKonstantin Kudryashovは、以下の2つの相補的なBDDフレームワークを提唱している。

  1. SpecBDD

  2. StoryBDD

SpecBDD

SpecBDDは、低レベルの実装ユニットとその間の通信に関する仕様を開発することに重点を置いている。
低レベルなテスト、いわゆる単体テストで用いられる。

SpecBDDフレームワークの例:RSpec

# in spec/calculator_spec.rb
RSpec.describe Calculator do
  describe '#add' do
    it 'returns the sum of its arguments' do
      expect(Calculator.new.add(1, 2)).to eq(3)
    end
  end
end

StoryBDD

StoryBDDは、アプリケーションの機能に関する、人間が読みやすいビジネス向け動作記述を作成する手法。DDDと機能テストを融合させた進化形。

StoryBDDでは、ステークホルダー向けのシナリオを作成する。これらのシナリオはテストではなく、ビジネス(ステークホルダー)にとって重要な機能以外を対象とするものではない。

シナリオはテストではないが、StoryBDD用のフレームワークを使うことで受け入れフェーズのテストを自動化できる。

StoryBDDのフレームワークの例

Cucumber

Gherkin形式の Given / When / Then で振る舞いを記述し、テストコードと対応付けて実行するBDDツール。
JavaScript、Python、Rubyなど複数言語に対応しており、PythonのBehaveやPytest-BDDもCucumberのコンポーネントを利用しているとされる

典型的には、次の形式(Gherkin記法)で仕様を書く。

Feature: ユーザーのログイン

  Scenario: 正しい認証情報でログインできる
    Given ログイン画面を開いている
    When 正しいメールアドレスとパスワードを入力する
    Then マイページが表示される

StoryBDDでは、上記のような低レベルの記述から始めることができますが、最終的な目的は「コードをテストする」ことではなく、「ビジネスのドメイン領域を記述する」ことであるべきです。

BDDのツール

代表的なツール

Cucumber

Gherkin形式の Given / When / Then で振る舞いを記述し、テストコードと対応付けて実行するBDDツール。
JavaScript、Python、Rubyなど複数言語に対応しており、PythonのBehaveやPytest-BDDもCucumberのコンポーネントを利用しているとされる

Behave(Python)

pythonのBDDパッケージ

# -- FILE: features/example.feature
Feature: Showing off behave

  Scenario: Run a simple test
    Given we have behave installed
     When we implement 5 tests
     Then behave will test them for us!
# -- FILE: features/steps/example_steps.py
from behave import given, when, then, step

@given('we have behave installed')
def step_impl(context):
    pass

@when('we implement {number:d} tests')
def step_impl(context, number):  # -- NOTE: number is converted into integer
    assert number > 1 or number == 0
    context.tests_count = number

@then('behave will test them for us!')
def step_impl(context):
    assert context.failed is False
    assert context.tests_count >= 0

Pytest-BDD(Python)

# content of publish_article.feature

Feature: Blog
    A site where you can publish your articles.

    Scenario: Publishing the article
        Given I'm an author user
        And I have an article

        When I go to the article page
        And I press the publish button

        Then I should not see the error message
        And the article should be published
# content of test_publish_article.py

from pytest_bdd import scenario, given, when, then

@scenario('publish_article.feature', 'Publishing the article')
def test_publish():
    pass


@given("I'm an author user")
def author_user(auth, author):
    auth['user'] = author.user


@given("I have an article", target_fixture="article")
def article(author):
    return create_test_article(author=author)


@when("I go to the article page")
def go_to_article(article, browser):
    browser.visit(urljoin(browser.url, '/manage/articles/{0}/'.format(article.id)))


@when("I press the publish button")
def publish_article(browser):
    browser.find_by_css('button[name=publish]').first.click()


@then("I should not see the error message")
def no_error_message(browser):
    with pytest.raises(ElementDoesNotExist):
        browser.find_by_css('.message.error').first


@then("the article should be published")
def article_is_published(article):
    article.refresh()  # Refresh the object in the SQLAlchemy session
    assert article.is_published

RSpec(Ruby)

RSpecはRubyのテストフレームワーク。Gherkin記法は使わないがBDDを謳っている

Behaviour Driven Development for Ruby.
Making TDD Productive and Fun.
https://rspec.info/

RSpec Rails | RSpec 公式ドキュメント

RSpec.describe 'Post' do           #
  context 'before publication' do  # (almost) plain English
    it 'cannot have comments' do   #
      expect { Post.create.comments.create! }.to raise_error(ActiveRecord::RecordInvalid)  # test code
    end
  end
end