---
# System prepended metadata

title: pytest_bdd
tags: [python, 測試, pytest_bdd]

---

## 目錄
* 簡介
* Gherkin language 是什麼？
* 會使用套件與版本
* 建立簡單測試專案
    1. 建立 python 虛擬環境
    2. 安裝相關套件
    3. 撰寫測試腳本
    4. 撰寫測試程式集
* E2E
    1. selenium
    2. pytest fixture
    3. 建立簡單E2E測試
    4. 專案目錄
    5. 執行測試專案
* 參考資料


### 簡介
> 使用 pytest-bdd 搭配測試， 測試腳本是使用 Gherkin language。

### Gherkin language 是什麼？
> Gherkin 是使用自然語言（英文、中文）對操作行為與使用情境進行描述的簡單結構語法。

``` Gherkin
Feature: Login functionality 測試功能
 
Scenario: Login to system 情境描述
    Given  I visit login page 前置條件
    When I enter account and password 當事件發生
        And I press the login button
    Then  I should see home page 結果
```

### 會使用套件與版本
* python 3.9.0
* pytest 7.4.3
* pytest-bdd 7.0.1
* selenium 4.19.0

### 建立簡單測試專案
1. 建立專案資料夾
```
    mkdir tests
    cd tests
    mkdir features functional
```
> 目錄結構
```
測試專案結構
    tests
    │
    └──features
    │
    │
    └──functional
```

#### 建立 python 虛擬環境
1. 進入專案內
> 目前位置為 /tests
2. 建立 python 虛擬環境
```
    python3 -m venv test_venv(名稱)
```
> 目錄會出現 test_venv 資料夾
3. 啟動 python 虛擬環境
```
    source test_venv/bin/activate
```
4. 環境啟動成功在終端機的指令列前端會出現 (虛擬名稱)。
5. 停用虛擬環境指令
```
    deactivate
```

#### 安裝相關套件
位置： 虛擬環境內的 /tests 目錄下
1. 安裝 pytest
```
    pip install pytest
```
2. 安裝 pytest
```
    pip install pytest-bdd
```
4. 檢查套件是否安裝成功
```
    python -m pip list
```
> 會出現套件列表
```
Package            Version
------------------ -----------
pytest              7.4.3
pytest-bdd          7.0.1
```

#### 撰寫測試腳本
位置： tests/features 目錄下
1. 新增 login.feature 檔案。
2. 在 login.feature 內使用 Gherkin language 規範進行撰寫測試腳本。
``` Gherkin
Feature: Login functionality
 
Scenario: Login to system
    Given  I visit login page 
    When I enter account and password
    And I press the login button
    Then  I should see home page
```

#### 撰寫測試程式集
位置： tests/features 目錄下
1. 新增 test_login 檔案。
``` python
import pytest  
from pytest_bdd import scenarios, given, when, then

scenarios('login.feature') 

@given('I visit login page ')
def login():
    pass

@when('I enter account and password')
def set_account_password():
    pass

@when('I press the login button')
def click_button():
    pass

@then(parsers.parse('I should see the "{title}" page'))  
def login_success(driver, title):
    assert 1 == 1
```
2. 執行測試
```
    python3 -m pytest
```

### E2E
> E2E 是 End-to-end testing(端對端測試)的簡寫
> 使用 pytest-Bdd 搭配 selenium 進行網站的 E2E

#### selenium
> selenium 是自動化操作瀏覽器的工具，常用於測試與爬蟲。
> 安裝 selenium
```
    pip install selenium
```

#### pytest fixture
* fixture 是 pytest 特有的功能，是在測試程式準備階段被呼叫的函式，可用於處理環境設定，如資料庫連線、測試資料集等。fixture 函式會以依賴註入方式帶入測試函式內應用。
* 在特定情境，fixture 會使用 yield 傳送值，如瀏覽器物件的傳遞，為了能正確的關閉。
* yield 惰性求值(lazy evaluation) 的特性，在生成資料傳遞之後會回到函式的特性，來實現 xUnit-style setup/teardown 功能，在一個函式進行建立與關閉。

#### 建立簡單E2E測試
> 以登入測試為例
[完整範例程式](https://github.com/qnsak/example_pytest_bdd/tree/master)

#### 專案目錄
```
    tests
    │
    └──features
    │  │
    │  └──login.feature
    └──functional
        │
        └──test_login.py
```
1. login.feature
``` feature
Feature: Login functionality

  Scenario: Login to system  
    Given'I visit login page
    When I enter user in the account field
    And I enter 123456 in the password field
    And I press the login button
    Then I should see the hello
```
2. test_login.py
> 在 pytest-bdd 導入 selenium
``` python
import pytest  
from pytest_bdd import scenarios, given, when, then
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

@pytest.fixture
def driver():
    # 取得瀏覽器的設定資料
    browser_options = Options() 
    # 不顯示瀏覽器
    browser_options.add_argument('--headless') 
    # 启动浏览器
    browser = webdriver.Chrome(options=browser_options)
    # 等待瀏覽器開啟
    browser.implicitly_wait(20)
    # 送出瀏覽器物件
    yield browser
    # 关闭浏览器
    browser.quit()

scenarios('login.feature')  

@given(parsers.parse('I visit login page'))
def set_account(driver, path):
    driver.get('http://127.0.0.1:3005/'+path)

@when(parsers.parse('I enter {account} in the account field'))
def set_password(driver, account):
    driver.find_element(By.NAME, 'name').send_keys(account)
 
@when(parsers.parse('I enter {password} in the password field'))
def set_password(driver, password):
    driver.find_element(By.NAME, 'password').send_keys(password)

@when(parsers.parse('I press the login button'))
def click_login_button(driver):
    driver.find_element(By.NAME, 'login_button').click()

@then(parsers.parse('I should see the hello'))  
def login_success(driver, title):
    data = WebDriverWait(driver,100).until(EC.presence_of_element_located((By.XPATH, '/html/body/h1')))
    assert 'hello' == data.text
```

#### 執行測試專案
1. 安裝測試套件
``` shell
    # 進入測試專案，建議使用 python 虛擬環境
    cd tests 
    # 安裝套件
    pip install -r requirements.txt
```
2. 安裝簡單登入網頁
``` shell
    # 進入測試專案，建議使用 python 虛擬環境
    cd web 
    # 安裝套件
    pip install -r requirements.txt
```
3. 執行測試
``` shell
    # 到測試專案目錄執行測試
    pytest
```

## 參考資料
[PYTHON TESTING 101: PYTEST-BDD](https://automationpanda.com/2018/10/22/python-testing-101-pytest-bdd/)
[Pytest-BDD’s documentation!](https://pytest-bdd.readthedocs.io/en/stable/)
[pypi](https://pypi.org/project/pytest-bdd/)
[pytest-with-eric](https://pytest-with-eric.com/bdd/pytest-bdd/)
[從 0 開始培育成為自動化測試工程師的學習指南系列 第 23 篇](https://ithelp.ithome.com.tw/articles/10326971?sc=rss.iron)
[pytest fixture](https://blog.tzing.tw/posts/python-testing-pytest-fixture-91b547f2)
[automationpanda](https://automationpanda.com/2023/01/12/passing-test-inputs-into-pytest/)
[ithome - 關於Gherkin](https://ithelp.ithome.com.tw/articles/10226615)