├── blog ├── blog │ ├── __init__.py │ ├── wsgi.py │ ├── production_settings.py │ ├── urls.py │ └── settings.py ├── article │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── admin.py │ ├── templates │ │ ├── create_article.html │ │ └── detail.html │ ├── models.py │ └── views.py ├── runtime.txt ├── Procfile ├── .gitignore ├── requirements.txt └── manage.py ├── demo ├── calc │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── admin.py │ ├── apps.py │ ├── templates │ │ └── calculator.html │ ├── scalc.py │ ├── views.py │ ├── calculator.py │ └── tests.py ├── demo │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── .gitignore ├── manage.py ├── requirements.txt └── features │ ├── steps │ ├── zh_calc.py │ └── calc.py │ ├── zh_calc.feature │ └── calc.feature ├── jenkins.png ├── .gitignore ├── demo-calc1.png ├── demo-calc2.png ├── uncertainty.png ├── demo-reports.png ├── jenkins-myBuild.png ├── uncertainty_rd.png ├── welcome2django.png ├── uncertainty_agile.png ├── Systems_Engineering_Process_II.png ├── uncertainty_traditional_engineering.png ├── connecting-the-steps-with-the-interface.png ├── pyparsing ├── ex0.py ├── ex1.py ├── ex2.py ├── ex3.py ├── ex4.py └── ex5.py ├── test ├── features │ ├── 帳號.feature │ ├── steps │ │ ├── 步驟.py │ │ ├── steps.py │ │ └── account.py │ └── account.feature └── account │ ├── account2.py │ └── account1.py ├── README.md ├── types-of-tests.md ├── mysql-connector.md ├── django-jenkins.md ├── env.sh ├── environment.md ├── pyparsing_exercise.md ├── sqlite.md ├── unittest.md ├── behave.md ├── behave-django.md ├── pyparsing.md ├── executable-specifications.md ├── unittest.mock.md ├── jenkins.md ├── django.md └── experiment.md /blog/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/calc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/article/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.5.1 2 | -------------------------------------------------------------------------------- /demo/calc/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/article/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn --pythonpath blog blog.wsgi 2 | -------------------------------------------------------------------------------- /blog/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | staticfiles 4 | db.sqlite3 5 | -------------------------------------------------------------------------------- /jenkins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/jenkins.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | db.sqlite3 3 | .python-version 4 | *.pyc 5 | .*.swp 6 | -------------------------------------------------------------------------------- /demo-calc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/demo-calc1.png -------------------------------------------------------------------------------- /demo-calc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/demo-calc2.png -------------------------------------------------------------------------------- /uncertainty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/uncertainty.png -------------------------------------------------------------------------------- /demo-reports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/demo-reports.png -------------------------------------------------------------------------------- /demo/calc/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /blog/article/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/calc/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /jenkins-myBuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/jenkins-myBuild.png -------------------------------------------------------------------------------- /uncertainty_rd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/uncertainty_rd.png -------------------------------------------------------------------------------- /welcome2django.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/welcome2django.png -------------------------------------------------------------------------------- /uncertainty_agile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/uncertainty_agile.png -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | db.sqlite3 3 | .python-version 4 | *.pyc 5 | .*.swp 6 | reports/ 7 | -------------------------------------------------------------------------------- /Systems_Engineering_Process_II.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/Systems_Engineering_Process_II.png -------------------------------------------------------------------------------- /demo/calc/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CalcConfig(AppConfig): 5 | name = 'calc' 6 | -------------------------------------------------------------------------------- /blog/article/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArticleConfig(AppConfig): 5 | name = 'article' 6 | -------------------------------------------------------------------------------- /uncertainty_traditional_engineering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/uncertainty_traditional_engineering.png -------------------------------------------------------------------------------- /connecting-the-steps-with-the-interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugolu/learn-test/HEAD/connecting-the-steps-with-the-interface.png -------------------------------------------------------------------------------- /blog/requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.4.1 2 | dj-static==0.0.6 3 | Django==1.9.7 4 | django-grappelli==2.8.1 5 | gunicorn==19.6.0 6 | static3==0.7.0 7 | psycopg2==2.6.1 8 | -------------------------------------------------------------------------------- /blog/article/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from article.models import Article, Category 3 | 4 | # Register your models here. 5 | admin.site.register(Article) 6 | admin.site.register(Category) 7 | -------------------------------------------------------------------------------- /demo/calc/templates/calculator.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /pyparsing/ex0.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, StringEnd, alphas 2 | 3 | noEnd = Word(alphas) 4 | print(noEnd.parseString('Dorking...')) 5 | 6 | withEnd = Word(alphas) + StringEnd() 7 | print(withEnd.parseString('Dorking...')) 8 | -------------------------------------------------------------------------------- /blog/article/templates/create_article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | {% csrf_token %} 6 | {{ form.as_p }} 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /blog/article/templates/detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

{{ article.title }}

5 | {% if article.pk == 1 %} 6 | {{ article.content|upper }} 7 | {% else %} 8 | {{ article.content }} 9 | {% endif %} 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/calc/scalc.py: -------------------------------------------------------------------------------- 1 | class SimpleCalculator: 2 | 3 | def add(self, a, b): 4 | return a+b 5 | 6 | def sub(self, a, b): 7 | return a-b 8 | 9 | def mul(self, a, b): 10 | return a*b 11 | 12 | def div(self, a, b): 13 | return a/b 14 | -------------------------------------------------------------------------------- /blog/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==1.4.6 2 | behave==1.2.5 3 | behave-django==0.3.0 4 | colorama==0.3.7 5 | configparser==3.5.0 6 | coverage==4.1 7 | Django==1.9.7 8 | django-jenkins==0.19.0 9 | lazy-object-proxy==1.2.2 10 | nose==1.3.7 11 | parse==1.6.6 12 | parse-type==0.3.4 13 | pylint==1.5.6 14 | PyMySQL==0.7.4 15 | pyparsing==2.1.5 16 | six==1.10.0 17 | wrapt==1.10.8 18 | -------------------------------------------------------------------------------- /demo/calc/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse 3 | from calc.calculator import Calculator 4 | 5 | # Create your views here. 6 | def calc(request): 7 | value = '' 8 | 9 | if request.method == 'POST': 10 | calc = Calculator() 11 | expr = request.POST['expr'] 12 | value = calc.evalString(expr) 13 | 14 | return render(request, 'calculator.html', {'value': value}) 15 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /blog/blog/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blog project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | from dj_static import Cling 15 | 16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") 17 | 18 | application = Cling(get_wsgi_application()) 19 | -------------------------------------------------------------------------------- /blog/article/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | class Category(models.Model): 5 | name = models.CharField(u'Name', max_length=50) 6 | 7 | def __str__(self): 8 | return self.name 9 | 10 | class Article(models.Model): 11 | content = models.TextField(u'Content') 12 | title = models.CharField(u'Title', max_length=50) 13 | category = models.ForeignKey('Category', blank=True, null=True) 14 | 15 | def __str__(self): 16 | return self.title 17 | -------------------------------------------------------------------------------- /blog/blog/production_settings.py: -------------------------------------------------------------------------------- 1 | # Import all default settings. 2 | from .settings import * 3 | 4 | import dj_database_url 5 | DATABASES = { 6 | 'default': dj_database_url.config() 7 | } 8 | 9 | # Static asset configuration. 10 | STATIC_ROOT = 'staticfiles' 11 | 12 | # Honor the 'X-Forwarded-Proto' header for request.is_secure(). 13 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 14 | 15 | # Allow all host headers. 16 | ALLOWED_HOSTS = ['*'] 17 | 18 | # Turn off DEBUG mode. 19 | DEBUG = False 20 | -------------------------------------------------------------------------------- /pyparsing/ex1.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, nums, alphas, Forward, Suppress, ZeroOrMore 2 | import unittest 3 | 4 | intStack = [] 5 | def pushStack(s, l, t): 6 | intStack.append(t[0]) 7 | 8 | atom = Word(nums).setParseAction(pushStack) | Suppress(Word(alphas)) 9 | expr = Forward() 10 | expr << atom + ZeroOrMore(expr) 11 | 12 | def evalString(string): 13 | return expr.parseString(string).asList() 14 | 15 | class TestEx1(unittest.TestCase): 16 | 17 | def test_evalString(self): 18 | self.assertEqual(evalString("12ab34cd56ef"), ['12', '34', '56']) 19 | self.assertEqual(evalString("ab12cd34ef56"), ['12', '34', '56']) 20 | 21 | -------------------------------------------------------------------------------- /test/features/帳號.feature: -------------------------------------------------------------------------------- 1 | # language: zh-TW 2 | 3 | 功能: 用戶帳號 4 | 為了買賣商品 5 | 身為買家或賣家 6 | 我想要有一個電子商務網站帳號 7 | 8 | 場景: 用正確的帳號跟密碼登入 9 | 假設< 帳號django與密碼django123已註冊 10 | 當< 我用django與密碼django123登入 11 | 那麼< 我得到登入結果:成功 12 | 13 | 場景: 用不正確的帳號跟密碼登入 14 | 假設< 帳號django與密碼django123已註冊 15 | 當< 我用django與密碼abcdef123登入 16 | 那麼< 我得到登入結果:失敗 17 | 18 | 場景大綱: 帳號與密碼必須大於5個字元 19 | 當< 嘗試用帳號與密碼註冊 20 | 那麼< 我得到註冊結果: 21 | 22 | 例子: 一些帳號與密碼 23 | | username | password | result | 24 | | abc | 123456 | 無效的帳號或密碼 | 25 | | abcedf | 123 | 無效的帳號或密碼 | 26 | | abc | 123 | 無效的帳號或密碼 | 27 | | abcdef | 123456 | 帳號建立 | 28 | -------------------------------------------------------------------------------- /test/features/steps/步驟.py: -------------------------------------------------------------------------------- 1 | from account import * 2 | 3 | @given(u'< 帳號{username}與密碼{password}已註冊') 4 | def step_impl(context, username, password): 5 | account_insert(username, password) 6 | 7 | @when(u'< 我用{username}與密碼{password}登入') 8 | def step_impl(context, username, password): 9 | if account_login(username, password) == True: 10 | context.result = "成功" 11 | else: 12 | context.result = "失敗" 13 | 14 | @then(u'< 我得到登入結果:{result}') 15 | def step_impl(context, result): 16 | assert(context.result == result) 17 | 18 | @when(u'< 嘗試用帳號{username}與密碼{password}註冊') 19 | def step_impl(context, username, password): 20 | if account_register(username, password) == True: 21 | context.result = "帳號建立" 22 | else: 23 | context.result = "無效的帳號或密碼" 24 | 25 | @then(u'< 我得到註冊結果:{result}') 26 | def step_impl(context, result): 27 | assert(context.result == result) 28 | -------------------------------------------------------------------------------- /demo/features/steps/zh_calc.py: -------------------------------------------------------------------------------- 1 | from calc.calculator import Calculator 2 | 3 | @given(u'< 我輸入{expr}') 4 | def step_impl(context, expr): 5 | context.expr = expr 6 | 7 | @when(u'< 我按下等號按鈕') 8 | def step_impl(context): 9 | calc = Calculator() 10 | context.answer = calc.evalString(context.expr) 11 | 12 | @then(u'< 我得到的答案是{answer}') 13 | def step_impl(context, answer): 14 | try: 15 | ans = float(answer) 16 | except ValueError: 17 | ans = answer 18 | 19 | assert context.answer == ans 20 | 21 | @when(u'< 我先輸入{expr1}') 22 | def step_impl(context, expr1): 23 | calc = Calculator() 24 | context.answer1 = calc.evalString(expr1) 25 | 26 | @when(u'< 我再輸入{expr2}') 27 | def step_impl(context, expr2): 28 | calc = Calculator() 29 | context.answer2 = calc.evalString(expr2) 30 | 31 | @then(u'< 我得到相同的答案') 32 | def step_impl(context): 33 | assert context.answer1 == context.answer2 34 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | """demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | import calc.views as calc_views 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', admin.site.urls), 22 | url(r'^$', calc_views.calc), 23 | ] 24 | -------------------------------------------------------------------------------- /blog/article/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse, HttpResponseRedirect 3 | from article.models import Article 4 | from django import forms 5 | 6 | # Create your views here. 7 | def home(request): 8 | s = "Hello World!" 9 | return HttpResponse(s) 10 | 11 | def detail(request, pk): 12 | article = Article.objects.get(pk=int(pk)) 13 | return render(request, 'detail.html', {'article': article}) 14 | 15 | class ArticleForm(forms.ModelForm): 16 | class Meta: 17 | model = Article 18 | fields = ['title', 'content', ] 19 | 20 | def create(request): 21 | if request.method == 'POST': 22 | form = ArticleForm(request.POST) 23 | if form.is_valid(): 24 | new_article = form.save() 25 | return HttpResponseRedirect('/article/' + str(new_article.pk)) 26 | 27 | form = ArticleForm() 28 | return render(request, 'create_article.html', {'form': form}) 29 | -------------------------------------------------------------------------------- /demo/features/steps/calc.py: -------------------------------------------------------------------------------- 1 | from calc.calculator import Calculator 2 | 3 | @given(u'I enter {expr}') 4 | def step_impl(context, expr): 5 | context.expr = expr 6 | 7 | @when(u'I press "=" button') 8 | def step_impl(context): 9 | calc = Calculator() 10 | context.answer = calc.evalString(context.expr) 11 | 12 | @then(u'I get the answer {answer}') 13 | def step_impl(context, answer): 14 | try: 15 | ans = float(answer) 16 | except ValueError: 17 | ans = answer 18 | 19 | assert context.answer == ans 20 | 21 | @when(u'I enter {expr1} first') 22 | def step_impl(context, expr1): 23 | calc = Calculator() 24 | context.answer1 = calc.evalString(expr1) 25 | 26 | @when(u'I enter {expr2} again') 27 | def step_impl(context, expr2): 28 | calc = Calculator() 29 | context.answer2 = calc.evalString(expr2) 30 | 31 | @then(u'I get the same answer') 32 | def step_impl(context): 33 | assert context.answer1 == context.answer2 34 | -------------------------------------------------------------------------------- /test/account/account2.py: -------------------------------------------------------------------------------- 1 | import pymysql.cursors 2 | 3 | cnx = pymysql.Connect(user='root', password='000000', host='127.0.0.1', db='test') 4 | cursor = cnx.cursor() 5 | 6 | def login_check(username, password): 7 | query = "SELECT password FROM account WHERE username='%s'" % username 8 | cursor.execute(query) 9 | row = cursor.fetchone() 10 | if row is not None: 11 | (pw,) = row 12 | return "successful" if pw == password else "failed" 13 | else: 14 | return "failed" 15 | 16 | import unittest 17 | class TestAccount(unittest.TestCase): 18 | 19 | def test_login_with_correct_username_password(self): 20 | self.assertEqual(login_check("abcdef", "123456"), "successful") 21 | 22 | def test_login_with_invalid_username(self): 23 | self.assertEqual(login_check("ABCEDF", "123456"), "failed") 24 | 25 | def test_login_with_invalid_password(self): 26 | self.assertEqual(login_check("abcedf", "000000"), "failed") 27 | -------------------------------------------------------------------------------- /test/account/account1.py: -------------------------------------------------------------------------------- 1 | import mysql.connector 2 | 3 | cnx = mysql.connector.connect(user='root', password='000000', host='127.0.0.1', database='test') 4 | cursor = cnx.cursor() 5 | 6 | def login_check(username, password): 7 | query = "SELECT password FROM account WHERE username='%s'" % username 8 | cursor.execute(query) 9 | row = cursor.fetchone() 10 | if row is not None: 11 | (pw,) = row 12 | return "successful" if pw == password else "failed" 13 | else: 14 | return "failed" 15 | 16 | import unittest 17 | class TestAccount(unittest.TestCase): 18 | 19 | def test_login_with_correct_username_password(self): 20 | self.assertEqual(login_check("abcdef", "123456"), "successful") 21 | 22 | def test_login_with_invalid_username(self): 23 | self.assertEqual(login_check("ABCEDF", "123456"), "failed") 24 | 25 | def test_login_with_invalid_password(self): 26 | self.assertEqual(login_check("abcedf", "000000"), "failed") 27 | -------------------------------------------------------------------------------- /test/features/steps/steps.py: -------------------------------------------------------------------------------- 1 | from account import * 2 | 3 | @given(u'an username {username} with the password {password} is registered') 4 | def step_impl(context, username, password): 5 | account_insert(username, password) 6 | 7 | @when(u'I login as {username} and give the password {password}') 8 | def step_impl(context, username, password): 9 | if account_login(username, password) == True: 10 | context.result = "successful" 11 | else: 12 | context.result = "failed" 13 | 14 | @then(u'I get the login result: {result}') 15 | def step_impl(context, result): 16 | assert(context.result == result) 17 | 18 | @when(u'try to register a name {username} with a password {password}') 19 | def step_impl(context, username, password): 20 | if account_register(username, password) == True: 21 | context.result = "successful" 22 | else: 23 | context.result = "invalid username or password" 24 | 25 | @then(u'I get the register result: {result}') 26 | def step_impl(context, result): 27 | assert(context.result == result) 28 | -------------------------------------------------------------------------------- /blog/blog/urls.py: -------------------------------------------------------------------------------- 1 | """blog URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | #url(r'^grappelli/', include('grappelli.urls')), 21 | url(r'^admin/', admin.site.urls), 22 | url(r'^$', 'article.views.home'), 23 | url(r'^article/(?P[0-9]+)/$', 'article.views.detail'), 24 | url(r'^create/$', 'article.views.create'), 25 | ] 26 | -------------------------------------------------------------------------------- /test/features/account.feature: -------------------------------------------------------------------------------- 1 | Feature: User account 2 | In order to buy or sell commodities 3 | As a buyer or seller 4 | I want to have a account in the Ecommerce website 5 | 6 | Scenario: Login as correct username and password 7 | Given an username django with the password django123 is registered 8 | When I login as django and give the password django123 9 | Then I get the login result: successful 10 | 11 | Scenario: Login as incorrect username and password 12 | Given an username django with the password django123 is registered 13 | When I login as django and give the password abcdef123 14 | Then I get the login result: failed 15 | 16 | Scenario Outline: username and password must be large than 5 characters 17 | When try to register a name with a password 18 | Then I get the register result: 19 | 20 | Examples: some usernames and passwords 21 | | username | password | result | 22 | | abc | 123456 | invalid username or password | 23 | | abcedf | 123 | invalid username or password | 24 | | abc | 123 | invalid username or password | 25 | | abcdef | 123456 | successful | 26 | -------------------------------------------------------------------------------- /blog/article/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-06-10 03:43 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Article', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('content', models.TextField(verbose_name='Content')), 22 | ('title', models.CharField(max_length=50, verbose_name='Title')), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Category', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('name', models.CharField(max_length=50, verbose_name='Name')), 30 | ], 31 | ), 32 | migrations.AddField( 33 | model_name='article', 34 | name='category', 35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='article.Category'), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 關於測試,我說的其實是...... 2 | 3 | ## Slide 4 | - [關於測試,我說的其實是......](http://www.slideshare.net/hugolu/ss-63936508) 5 | 6 | ## Demo 7 | 8 | - [demo](demo.md) - 透過開發網頁計算的的例子,串連 BDD, TDD, Test Double, Test Automation, Web Development 多項技術 9 |  - [source code](demo/) 10 | - [環境設定](environment.md) 11 | 12 | ## 參考 13 | 14 | - 重要觀念 15 | - [可執行的需求](executable-specifications.md) - 這次學習的濫觴,薄薄一本言簡意賅,超推! 16 | - [測試的種類](types-of-tests.md) - 分類各種測試 17 | - 測試基本技能 18 | - BDD 19 | - [Behave](behave.md) - Behaviour-Driven Development, Python style 20 | - TDD 21 | - [unittest](unittest.md) - python 單元測試框架 22 | - [unittest.mock](unittest.mock.md) - python 單元測試替身,切開元件間的相依性 23 | - BDD/TDD 實驗 24 | - [實驗](experiment.md) - 使用 BDD/TDD 開發網頁登入、註冊核心功能,不含網頁呈現部分 25 | - [test/](test/) - BDD/TDD 實驗的程式碼 26 | - Django 網站開發 27 | - [django](django.md) - 從無到有,一步步架設網頁,範例程式在 [blog/](blog/) 28 | - [behave-django](behave-django.md) - 支援 Djando 的 BDD 測試框架 29 | - [django-jenkins](django-jenkins.md) - 支援 Django 的 Jenkins 套件,用來自動化測試與產生報表 30 | - [mysql-connector](mysql-connector.md) - MySQL driver for Python 31 | - [sqlite](sqlite.md) - sqlite3 使用說明 32 | - [Jenkins](jenkins.md) - 透過自動化測試、自動化建置與部署,提高敏捷開發的可能性 33 | - pyparsing 34 | - [pyparsing](pyparsing.md) - 翻譯說明文件 35 | - [pyparsing 練習](pyparsing_exercise.md) - 透過幾個練習,熟悉 pyparsing 解析字串的功能 36 | - [pyparsing/](pyparsing/) - pyparsing 練習的程式碼 37 | 38 | -------------------------------------------------------------------------------- /demo/features/zh_calc.feature: -------------------------------------------------------------------------------- 1 | # language: zh-TW 2 | 功能: 網頁計算機 3 | 4 | 身為一個學生 5 | 為了完成家庭作業 6 | 我想要做算術運算 7 | 8 | 場景大綱: 做簡單的運算 9 | 假設< 我輸入 10 | 當< 我按下等號按鈕 11 | 那麼< 我得到的答案是 12 | 13 | 例子: 14 | | expression | answer | 15 | | 3 + 2 | 5 | 16 | | 3 - 2 | 1 | 17 | | 3 * 2 | 6 | 18 | | 3 / 2 | 1.5 | 19 | | 3 +-*/ 2 | Invalid Input | 20 | | hello world | Invalid Input | 21 | 22 | 場景大綱: 滿足交換律 23 | 當< 我先輸入 24 | 當< 我再輸入 25 | 那麼< 我得到相同的答案 26 | 27 | 例子: 28 | | expression1 | expression2 | 29 | | 3 + 4 | 4 + 3 | 30 | | 2 * 5 | 5 * 2 | 31 | 32 | 場景大綱: 滿足結合律 33 | 當< 我先輸入 34 | 當< 我再輸入 35 | 那麼< 我得到相同的答案 36 | 37 | 例子: 38 | | expression1 | expression2 | 39 | | (2 + 3) + 4 | 2 + (3 + 4) | 40 | | 2 * (3 * 4) | (2 * 3) * 4 | 41 | 42 | 場景大綱: 滿足結合律 43 | 當< 我先輸入 44 | 當< 我再輸入 45 | 那麼< 我得到相同的答案 46 | 47 | 例子: 48 | | expression1 | expression2 | 49 | | 2 * (1 + 3) | (2*1) + (2*3) | 50 | | (1 + 3) * 2 | (1*2) + (3*2) | 51 | -------------------------------------------------------------------------------- /types-of-tests.md: -------------------------------------------------------------------------------- 1 | # 測試的分類 2 | 3 | ## 業務導向且支持開發過程的測試 4 | 5 | ![測試象限圖](http://continuousdelivery.com/images/test-quadrant.png) 6 | (圖片來自:http://continuousdelivery.com/foundations/test-automation/) 7 | 8 | ### 驗收測試 9 | - 確保用戶故事的驗收條件得到滿足 10 | - 做對的事情 (the the right thing) 11 | - 由用戶寫測試腳本,開發人員和測試人員努力實現這些腳本 12 | - Happy Path: Given-When-Then 13 | - 自動化驗收測試 14 | - 加速反應速度 15 | - 減少測試人員負擔 16 | - 提高探索性測試與高價值的活動 17 | - 支援回歸測試 18 | - 活文件 (不斷更新的規格書) 19 | 20 | ## 技術導向且支持開發過程的測試 21 | 22 | ### 單元測試 23 | - 單元測試單獨測試一段特定的程式碼 24 | - 把事情做對 (do the thing right) 25 | - 常常倚賴測試替身 (test double) 模擬系統其他部分 26 | - 沒有系統組件間的交互,執行速度快 27 | - 偵測修改程式碼是否破獲現有的功能 28 | 29 | ### 組件測試 30 | - 用於測試功能集合,捕捉系統不同部分交互產生的缺陷 31 | - 需要執行更多 I/O 操作、連接資料庫、檔案系統等 32 | 33 | ### 部署測試 34 | - 用於檢查部署過程是否正常 35 | - 應用程式是否是否正確安裝、配置,能否與所需的服務連接,得到相對的回應 36 | 37 | ## 業務導向且評價專案的測試 38 | 39 | ### 展示 40 | - 每次迭代結束時,敏捷開發團隊向用戶展示其開發完成的新功能 41 | - 頻繁地向客戶演示功能,儘早發現對需求規格的誤解,或修正有問題的規格 42 | 43 | ### 探索性測試 44 | - 創造學習的過程,不只是發現缺陷 45 | - 創建新的自動化測試,用來覆蓋新的需求 46 | 47 | ### 易用性測試 48 | - 驗證用戶能否容易使用軟體完成工作 49 | - 情境調查:觀察使用者操作過程,收集量化數據,如使用者多久時間完成任務、按了多少次錯誤的按鈕、試用者自評滿意度 50 | - Beta測試:網站同時運行多個版本,收集、統計、分析新功能的使用情形,讓功能適者生存不斷演進 51 | 52 | ## 技術導向且評價專案的測試 53 | 54 | ### 非功能性測試 55 | - 除了功能之外的系統品質測試,如容量、可用性、安全性 56 | - 客戶不關心非功能需求,但當他們意識這方面的問題,事情往往一發不可收拾 57 | - 容量:網站因為容量問題,停止提供服務 58 | - 安全性:用戶個資外洩、信用卡被盜刷 59 | 60 | ## 開發流程 61 | - 客戶、分析師、測試人員**定義**驗收條件 62 | - 測試人員與開發人員基於驗收條件,**實現**驗收測試自動化 63 | - 開發人員編寫程式**滿足**驗收條件 64 | - 只要有自動化測試失敗,無論是單元測試、組件測試、驗收測試,開發人員要優先處理並修復問題 65 | 66 | ---- 67 | ## 參考 68 | - https://continuousdelivery.com/foundations/test-automation/ 69 | - https://www.thoughtworks.com/continuous-delivery 70 | -------------------------------------------------------------------------------- /demo/features/calc.feature: -------------------------------------------------------------------------------- 1 | Feature: Web calculator 2 | 3 | As a student 4 | In order to finish my homework 5 | I want to do arithmatical operations 6 | 7 | Scenario Outline: do simple operations 8 | Given I enter 9 | When I press "=" button 10 | Then I get the answer 11 | 12 | Examples: 13 | | expression | answer | 14 | | 3 + 2 | 5 | 15 | | 3 - 2 | 1 | 16 | | 3 * 2 | 6 | 17 | | 3 / 2 | 1.5 | 18 | | 3 +-*/ 2 | Invalid Input | 19 | | hello world | Invalid Input | 20 | 21 | Scenario Outline: satisfy commutative property 22 | When I enter first 23 | And I enter again 24 | Then I get the same answer 25 | 26 | Examples: 27 | | expression1 | expression2 | 28 | | 3 + 4 | 4 + 3 | 29 | | 2 * 5 | 5 * 2 | 30 | 31 | Scenario Outline: satisfy associative property 32 | When I enter first 33 | And I enter again 34 | Then I get the same answer 35 | 36 | Examples: 37 | | expression1 | expression2 | 38 | | (2 + 3) + 4 | 2 + (3 + 4) | 39 | | 2 * (3 * 4) | (2 * 3) * 4 | 40 | 41 | Scenario Outline: satisfy distributive property 42 | When I enter first 43 | And I enter again 44 | Then I get the same answer 45 | 46 | Examples: 47 | | expression1 | expression2 | 48 | | 2 * (1 + 3) | (2*1) + (2*3) | 49 | | (1 + 3) * 2 | (1*2) + (3*2) | 50 | -------------------------------------------------------------------------------- /demo/calc/calculator.py: -------------------------------------------------------------------------------- 1 | from pyparsing import nums, Word, StringEnd, ParseException, Literal, ZeroOrMore, Forward 2 | from calc.scalc import SimpleCalculator 3 | 4 | """ 5 | integer :: '0'...'9'* 6 | addop :: '+' | '-' 7 | mulop :: '*' | '/' 8 | atom :: integer | '(' + expr + ')' 9 | term :: atom [mulop atom]* 10 | expr :: term [addop term]* 11 | """ 12 | 13 | class Calculator: 14 | 15 | def __init__(self, calc = SimpleCalculator()): 16 | self.exprStack = [] 17 | 18 | def pushStack(s, l, t): 19 | self.exprStack.append(t[0]) 20 | 21 | integer = Word(nums).addParseAction(pushStack) 22 | addop = Literal('+') | Literal('-') 23 | mulop = Literal('*') | Literal('/') 24 | lpar = Literal('(') 25 | rpar = Literal(')') 26 | 27 | expr = Forward() 28 | atom = integer | lpar + expr + rpar 29 | term = atom + ZeroOrMore((mulop + atom).addParseAction(pushStack)) 30 | expr << term + ZeroOrMore((addop + term).addParseAction(pushStack)) 31 | self.expr = expr + StringEnd() 32 | 33 | self.opfun = { 34 | '+' : (lambda a, b: calc.add(a,b)), 35 | '-' : (lambda a, b: calc.sub(a,b)), 36 | '*' : (lambda a, b: calc.mul(a,b)), 37 | '/' : (lambda a, b: calc.div(a,b)) } 38 | 39 | def parseString(self, string): 40 | self.exprStack = [] 41 | self.expr.parseString(string) 42 | return self.exprStack 43 | 44 | def evalStack(self, stack): 45 | op = stack.pop() 46 | if op in '+-*/': 47 | op2 = self.evalStack(stack) 48 | op1 = self.evalStack(stack) 49 | return self.opfun[op](op1, op2) 50 | else: 51 | return float(op) 52 | 53 | def evalString(self, string): 54 | try: 55 | self.parseString(string) 56 | return self.evalStack(self.exprStack) 57 | except ParseException: 58 | return 'Invalid Input' 59 | -------------------------------------------------------------------------------- /pyparsing/ex2.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, Literal, nums, ZeroOrMore 2 | import unittest 3 | 4 | """ 5 | op :: '+' | '-' | '*' | '/' 6 | num :: '0'...'9'+ 7 | expr :: num + [op + num]* 8 | """ 9 | 10 | exprStack = [] 11 | def pushFirst(s, l, t): 12 | exprStack.append(t[0]) 13 | 14 | add = Literal('+') 15 | sub = Literal('-') 16 | mul = Literal('*') 17 | div = Literal('/') 18 | op = add | sub | mul | div 19 | 20 | atom = Word(nums).addParseAction(pushFirst) 21 | expr = atom + ZeroOrMore((op + atom).addParseAction(pushFirst)) 22 | 23 | def parseString(string): 24 | global exprStack 25 | exprStack = [] 26 | expr.parseString(string) 27 | return exprStack 28 | 29 | opf = { '+' : (lambda a, b: a + b), 30 | '-' : (lambda a, b: a - b), 31 | '*' : (lambda a, b: a * b), 32 | '/' : (lambda a, b: a / b) } 33 | 34 | def evalStack(stack): 35 | op = stack.pop() 36 | if op in '+-*/': 37 | op2 = evalStack(stack) 38 | op1 = evalStack(stack) 39 | return opf[op](op1, op2) 40 | else: 41 | return float(op) 42 | 43 | def evalString(string): 44 | stack = parseString(string) 45 | result = evalStack(stack) 46 | return result 47 | 48 | class TestEx2(unittest.TestCase): 49 | 50 | def test_parseString(self): 51 | self.assertEqual(parseString('6+3'), ['6', '3', '+']) 52 | self.assertEqual(parseString('6-3'), ['6', '3', '-']) 53 | self.assertEqual(parseString('6*3'), ['6', '3', '*']) 54 | self.assertEqual(parseString('6/3'), ['6', '3', '/']) 55 | 56 | def test_evalStack(self): 57 | self.assertEqual(evalStack(['6', '3', '+']), 9.0) 58 | self.assertEqual(evalStack(['6', '3', '-']), 3.0) 59 | self.assertEqual(evalStack(['6', '3', '*']), 18.0) 60 | self.assertEqual(evalStack(['6', '3', '/']), 2.0) 61 | 62 | def test_evalString(self): 63 | self.assertEqual(evalString('6+3'), 9.0) 64 | self.assertEqual(evalString('6-3'), 3.0) 65 | self.assertEqual(evalString('6*3'), 18.0) 66 | self.assertEqual(evalString('6/3'), 2.0) 67 | 68 | def test_multiple_op(self): 69 | self.assertEqual(evalString('6+3+2'), 11.0) 70 | self.assertEqual(evalString('6-3-2'), 1.0) 71 | self.assertEqual(evalString('6*3*2'), 36.0) 72 | self.assertEqual(evalString('6/3/2'), 1.0) 73 | -------------------------------------------------------------------------------- /pyparsing/ex3.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, Literal, nums, ZeroOrMore 2 | import unittest 3 | 4 | """ 5 | op :: '+' | '-' | '*' | '/' 6 | atom :: '0'...'9'+ 7 | term :: atom + [mulop + atom]* 8 | expr :: term + [addop + term]* 9 | """ 10 | 11 | exprStack = [] 12 | def pushFirst(s, l, t): 13 | exprStack.append(t[0]) 14 | 15 | add = Literal('+') 16 | sub = Literal('-') 17 | mul = Literal('*') 18 | div = Literal('/') 19 | addop = add | sub 20 | mulop = mul | div 21 | 22 | atom = Word(nums).addParseAction(pushFirst) 23 | term = atom + ZeroOrMore((mulop + atom).addParseAction(pushFirst)) 24 | expr = term + ZeroOrMore((addop + term).addParseAction(pushFirst)) 25 | 26 | def parseString(string): 27 | global exprStack 28 | exprStack = [] 29 | expr.parseString(string) 30 | return exprStack 31 | 32 | opf = { '+' : (lambda a, b: a + b), 33 | '-' : (lambda a, b: a - b), 34 | '*' : (lambda a, b: a * b), 35 | '/' : (lambda a, b: a / b) } 36 | 37 | def evalStack(stack): 38 | op = stack.pop() 39 | if op in '+-*/': 40 | op2 = evalStack(stack) 41 | op1 = evalStack(stack) 42 | return opf[op](op1, op2) 43 | else: 44 | return float(op) 45 | 46 | def evalString(string): 47 | stack = parseString(string) 48 | result = evalStack(stack) 49 | return result 50 | 51 | class TestEx2(unittest.TestCase): 52 | 53 | def test_parseString(self): 54 | self.assertEqual(parseString('6+3'), ['6', '3', '+']) 55 | self.assertEqual(parseString('6-3'), ['6', '3', '-']) 56 | self.assertEqual(parseString('6*3'), ['6', '3', '*']) 57 | self.assertEqual(parseString('6/3'), ['6', '3', '/']) 58 | 59 | def test_evalStack(self): 60 | self.assertEqual(evalStack(['6', '3', '+']), 9.0) 61 | self.assertEqual(evalStack(['6', '3', '-']), 3.0) 62 | self.assertEqual(evalStack(['6', '3', '*']), 18.0) 63 | self.assertEqual(evalStack(['6', '3', '/']), 2.0) 64 | 65 | def test_evalString(self): 66 | self.assertEqual(evalString('6+3'), 9.0) 67 | self.assertEqual(evalString('6-3'), 3.0) 68 | self.assertEqual(evalString('6*3'), 18.0) 69 | self.assertEqual(evalString('6/3'), 2.0) 70 | 71 | def test_multiple_op(self): 72 | self.assertEqual(evalString('6+3+2'), 11.0) 73 | self.assertEqual(evalString('6-3-2'), 1.0) 74 | self.assertEqual(evalString('6*3*2'), 36.0) 75 | self.assertEqual(evalString('6/3/2'), 1.0) 76 | 77 | def test_order_of_operations(self): 78 | self.assertEqual(evalString('2+3*4'), 14.0) 79 | self.assertEqual(evalString('5+4*3-2/1'), 15.0) 80 | -------------------------------------------------------------------------------- /mysql-connector.md: -------------------------------------------------------------------------------- 1 | # MySQL connector for Python 2 | 3 | 目前找到兩種 4 | - [MySQL Connector](https://www.mysql.com/products/connector/) - 由 MySQL 官方提供 5 | - [MyMySQL Connector](https://github.com/PyMySQL/PyMySQL) - 第三方維護,純 Python code 6 | 7 | ## MySQL Connector 8 | 9 | - [MySQL Connector/Python Developer Guide](https://dev.mysql.com/doc/connector-python/en/) - mysql 官方說明 10 | 11 | ### 搜尋適合版本 12 | - [Index of Packages Matching 'mysql-connector'](https://pypi.python.org/pypi?%3Aaction=search&term=mysql-connector&submit=search) 13 | 14 | 找到 mysql-connector-python 2.0.4 15 | 16 | ### 下載 17 | - [mysql-connector-python 2.0.4](https://pypi.python.org/pypi/mysql-connector-python/2.0.4) 18 | 19 | 下載壓縮檔 20 | ```shell 21 | $ wget http://cdn.mysql.com/Downloads/Connector-Python/mysql-connector-python-2.0.4.zip#md5=3df394d89300db95163f17c843ef49df 22 | ``` 23 | 24 | 解壓縮 25 | ```shell 26 | $ unzip mysql-connector-python-2.0.4.zip 27 | ``` 28 | 29 | ### 安裝 30 | - [MySQL Connector/Python Developer Guide](http://dev.mysql.com/doc/connector-python/en/) 31 | - [Installing Connector/Python from a Source Distribution](http://dev.mysql.com/doc/connector-python/en/connector-python-installation-source.html) 32 | 33 | ```shell 34 | $ python setup.py install 35 | ``` 36 | 37 | ### 測試 38 | ``` 39 | $ python 40 | Python 3.5.1 (default, Jun 9 2016, 17:09:39) 41 | [GCC 4.9.2] on linux 42 | Type "help", "copyright", "credits" or "license" for more information. 43 | >>> import mysql.connector 44 | >>> 45 | ``` 46 | 47 | 或是在 test/account/ 目錄,執行以下單元測試 48 | ```shell 49 | $ python -m unittest -v account1 50 | test_login_with_correct_username_password (account1.TestAccount) ... ok 51 | test_login_with_invalid_password (account1.TestAccount) ... ok 52 | test_login_with_invalid_username (account1.TestAccount) ... ok 53 | 54 | ---------------------------------------------------------------------- 55 | Ran 3 tests in 0.004s 56 | 57 | OK 58 | ``` 59 | 60 | ## PyMySQL Connector 61 | 62 | - [PyMySQL github](https://github.com/PyMySQL/PyMySQL) 63 | 64 | ### 安裝 65 | ```shell 66 | $ pip install PyMySQL 67 | ``` 68 | 69 | ### 測試 70 | ```shell 71 | $ python 72 | Python 3.5.1 (default, Jun 9 2016, 17:09:39) 73 | [GCC 4.9.2] on linux 74 | Type "help", "copyright", "credits" or "license" for more information. 75 | >>> import pymysql.cursors 76 | >>> 77 | ``` 78 | 79 | 或是在 test/account/ 目錄,執行以下單元測試 80 | ```shell 81 | $ python -m unittest -v account2.py 82 | test_login_with_correct_username_password (account2.TestAccount) ... ok 83 | test_login_with_invalid_password (account2.TestAccount) ... ok 84 | test_login_with_invalid_username (account2.TestAccount) ... ok 85 | 86 | ---------------------------------------------------------------------- 87 | Ran 3 tests in 0.004s 88 | 89 | OK 90 | ``` 91 | -------------------------------------------------------------------------------- /pyparsing/ex4.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, Literal, nums, ZeroOrMore, Forward 2 | import unittest 3 | 4 | """ 5 | op :: '+' | '-' | '*' | '/' 6 | integer :: '0'...'9'+ 7 | atom :: integer | '(' expr ')' 8 | term :: atom [mulop atom]* 9 | expr :: term [addop term]* 10 | """ 11 | 12 | exprStack = [] 13 | def pushFirst(s, l, t): 14 | exprStack.append(t[0]) 15 | 16 | add = Literal('+') 17 | sub = Literal('-') 18 | mul = Literal('*') 19 | div = Literal('/') 20 | lpar = Literal('(') 21 | rpar = Literal(')') 22 | addop = add | sub 23 | mulop = mul | div 24 | 25 | expr = Forward() 26 | atom = Word(nums).addParseAction(pushFirst) | (lpar + expr + rpar) 27 | term = atom + ZeroOrMore((mulop + atom).addParseAction(pushFirst)) 28 | expr << term + ZeroOrMore((addop + term).addParseAction(pushFirst)) 29 | 30 | def parseString(string): 31 | global exprStack 32 | exprStack = [] 33 | expr.parseString(string) 34 | return exprStack 35 | 36 | opf = { '+' : (lambda a, b: a + b), 37 | '-' : (lambda a, b: a - b), 38 | '*' : (lambda a, b: a * b), 39 | '/' : (lambda a, b: a / b) } 40 | 41 | def evalStack(stack): 42 | op = stack.pop() 43 | if op in '+-*/': 44 | op2 = evalStack(stack) 45 | op1 = evalStack(stack) 46 | return opf[op](op1, op2) 47 | else: 48 | return float(op) 49 | 50 | def evalString(string): 51 | stack = parseString(string) 52 | result = evalStack(stack) 53 | return result 54 | 55 | class TestEx2(unittest.TestCase): 56 | 57 | def test_parseString(self): 58 | self.assertEqual(parseString('6+3'), ['6', '3', '+']) 59 | self.assertEqual(parseString('6-3'), ['6', '3', '-']) 60 | self.assertEqual(parseString('6*3'), ['6', '3', '*']) 61 | self.assertEqual(parseString('6/3'), ['6', '3', '/']) 62 | 63 | def test_evalStack(self): 64 | self.assertEqual(evalStack(['6', '3', '+']), 9.0) 65 | self.assertEqual(evalStack(['6', '3', '-']), 3.0) 66 | self.assertEqual(evalStack(['6', '3', '*']), 18.0) 67 | self.assertEqual(evalStack(['6', '3', '/']), 2.0) 68 | 69 | def test_evalString(self): 70 | self.assertEqual(evalString('6+3'), 9.0) 71 | self.assertEqual(evalString('6-3'), 3.0) 72 | self.assertEqual(evalString('6*3'), 18.0) 73 | self.assertEqual(evalString('6/3'), 2.0) 74 | 75 | def test_multiple_op(self): 76 | self.assertEqual(evalString('6+3+2'), 11.0) 77 | self.assertEqual(evalString('6-3-2'), 1.0) 78 | self.assertEqual(evalString('6*3*2'), 36.0) 79 | self.assertEqual(evalString('6/3/2'), 1.0) 80 | 81 | def test_order_of_operations(self): 82 | self.assertEqual(evalString('2+3*4'), 14.0) 83 | self.assertEqual(evalString('5+4*3-2/1'), 15.0) 84 | 85 | def test_parentheses(self): 86 | self.assertEqual(evalString('(2+3)*4'), 20.0) 87 | self.assertEqual(evalString('(5+4)*((3-2)-1)'), 0.0) 88 | -------------------------------------------------------------------------------- /django-jenkins.md: -------------------------------------------------------------------------------- 1 | # django-jenkins 2 | 3 | ## 開始之前 4 | 5 | 請參考 [django 設定環境](django.md#設定環境),準備開發環境、建立專案 (假設叫做 demo)... 6 | 7 | ```shell 8 | $ cd ~/myWorkspace 9 | $ source venv/bin/activate 10 | (venv) vagrant@debian:~/myWorkspace$ cd demo 11 | ``` 12 | 進入 django virtualenv 之後,繼續下面步驟 (以下省略提示符號前文字) 13 | 14 | ## 安裝 django-jenkins 15 | 16 | ```shell 17 | pip install django-jenkins 18 | ``` 19 | 20 | ## 設定專案 21 | 22 | 修改 demo/settings.py: 23 | - 加入 'django_jenkins' App 24 | - 加入要跑 jenkins test, report 的 App,例如 `calc` 25 | 26 | ```python 27 | INSTALLED_APPS = [ 28 | 'behave_django', 29 | 'django_jenkins', 30 | ... 31 | 'calc', 32 | ] 33 | 34 | # Jenkins settings 35 | PROJECT_APPS = [ 36 | 'calc', 37 | ] 38 | 39 | JENKINS_TASKS = ( 40 | 'django_jenkins.tasks.run_pylint', 41 | ) 42 | ``` 43 | 44 | ## 專案版控 45 | 46 | 初始化 git repository 47 | ```shell 48 | $ git init 49 | ``` 50 | 51 | 建立 .gitignore,設定哪些檔案不做版本追蹤 52 | ``` 53 | __pycache__ 54 | db.sqlite3 55 | .python-version 56 | *.pyc 57 | .*.swp 58 | reports/ 59 | ``` 60 | 61 | 上傳 git repository 62 | ```shell 63 | $ git add . 64 | $ git commit -m "first commit" 65 | ``` 66 | 67 | ## 設定 Jenkins server 68 | 69 | 開啟瀏覽器,連接 http://192.168.33.10:8080/ ([虛擬機](environment.md)) 70 | 71 | - Jenkins 管理首頁 72 | - New Item 73 | - Item name: `demo` 74 | - [x] Freestyle project 75 | - Source Code Management 76 | - [x] Git 77 | - Repository URL: `file:///home/vagrant/myWorkspace/demo` 78 | - Build Triggers 79 | - [x] Poll SCM 80 | - Schedule: `* * * * *` 81 | - Build Environment 82 | - [x] pyenv build wrapper 83 | - The Python version: `3.5.1` 84 | - Build 85 | - [x] Execute shell 86 | - Command: [shell command](#shell-command) 87 | - Post-build Actions 88 | - [x] Publish Cobertura Coverage Report 89 | - Cobertura xml report pattern: `reports/coverage.xml` 90 | - [x] Publish JUnit test result report 91 | - Test report XMLs: `reports/junit.xml` 92 | - [x] Report Violations 93 | - pylint: `reports/pylint.report` 94 | - Save 95 | 96 | ### shell command 97 | ``` 98 | PATH=$WORKSPACE/venv/bin:/usr/local/bin:$PATH 99 | 100 | if [ ! -d "venv" ]; then 101 | virtualenv venv 102 | fi 103 | . venv/bin/activate 104 | pip install -r requirements.txt 105 | 106 | python manage.py jenkins --enable-coverage 107 | ``` 108 | 109 | ## 觀看 reports 110 | ![Violations report](https://sites.google.com/site/kmmbvnr/home/django-jenkins-tutorial/jenkins-5.png) 111 | 112 | ![Coverage report](https://sites.google.com/site/kmmbvnr/_/rsrc/1286971838502/home/django-hudson-tutorial/8_coverage_results.png) 113 | 114 | ![test result](https://sites.google.com/site/kmmbvnr/_/rsrc/1327390871674/home/django-jenkins-tutorial/jenkins-6.png) 115 | 116 | ## 參考資料 117 | 118 | - [django-jenkins Github](https://github.com/kmmbvnr/django-jenkins) 119 | - [django-jenkins Tutorial](https://sites.google.com/site/kmmbvnr/home/django-jenkins-tutorial) 120 | -------------------------------------------------------------------------------- /demo/calc/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from calc.calculator import Calculator 3 | from calc.scalc import SimpleCalculator 4 | from unittest.mock import MagicMock 5 | 6 | # Create your tests here. 7 | class TestCalculator(TestCase): 8 | 9 | def setUp(self): 10 | """ 11 | add_dict = {(3,2) : 5, (4,6) : 10, (4,3) : 7, (2,2) : 4} 12 | sub_dict = {(3,2) : 1, (9,6) : 3, (9,3) : 6} 13 | mul_dict = {(3,2) : 6, (7,2) : 14, (6,4) : 24} 14 | div_dict = {(3,2) : 1.5, (2,1) : 2, (24,1) : 24} 15 | 16 | def add(*args): 17 | return add_dict[args] 18 | def sub(*args): 19 | return sub_dict[args] 20 | def mul(*args): 21 | return mul_dict[args] 22 | def div(*args): 23 | return div_dict[args] 24 | 25 | scalc = SimpleCalculator() 26 | scalc.add = MagicMock(side_effect = add) 27 | scalc.sub = MagicMock(side_effect = sub) 28 | scalc.mul = MagicMock(side_effect = mul) 29 | scalc.div = MagicMock(side_effect = div) 30 | 31 | self.calc = Calculator(scalc) 32 | """ 33 | self.calc = Calculator() 34 | 35 | def test_parseString(self): 36 | parseString = self.calc.parseString 37 | self.assertEqual(parseString('0'), ['0']) 38 | self.assertEqual(parseString('1'), ['1']) 39 | self.assertEqual(parseString('3+2'), ['3', '2', '+']) 40 | 41 | def test_evalStack(self): 42 | evalStack = self.calc.evalStack 43 | self.assertEqual(evalStack(['0']), 0) 44 | self.assertEqual(evalStack(['1']), 1) 45 | 46 | def test_evalString(self): 47 | evalString = self.calc.evalString 48 | self.assertEqual(evalString('0'), 0) 49 | self.assertEqual(evalString('1'), 1) 50 | 51 | def test_invalid_input(self): 52 | evalString = self.calc.evalString 53 | self.assertEqual(evalString('hello world'), 'Invalid Input') 54 | 55 | def test_num_op_num(self): 56 | evalString = self.calc.evalString 57 | self.assertEqual(evalString('3+2'), 5) 58 | self.assertEqual(evalString('3-2'), 1) 59 | self.assertEqual(evalString('3*2'), 6) 60 | self.assertEqual(evalString('3/2'), 1.5) 61 | 62 | def test_order_of_operations(self): 63 | evalString = self.calc.evalString 64 | self.assertEqual(evalString('4+3*2'), 10) 65 | self.assertEqual(evalString('9-3*2+2/1'), 5) 66 | 67 | def test_parentheses(self): 68 | evalString = self.calc.evalString 69 | self.assertEqual(evalString('(4+3)*2'), 14) 70 | self.assertEqual(evalString('(9-3)*(2+2)/1'), 24) 71 | 72 | def test_commutative_property(self): 73 | evalString = self.calc.evalString 74 | self.assertEqual(evalString('3+4'), evalString('4+3')) 75 | self.assertEqual(evalString('2*5'), evalString('5*2')) 76 | 77 | def test_associative_property(self): 78 | evalString = self.calc.evalString 79 | self.assertEqual(evalString('(5+2) + 1'), evalString('5 + (2+1)')) 80 | self.assertEqual(evalString('(5*2) * 3'), evalString('5 * (2*3)')) 81 | 82 | def test_distributive_property(self): 83 | evalString = self.calc.evalString 84 | self.assertEqual(evalString('2 * (1+3)'), evalString('(2*1) + (2*3)')) 85 | self.assertEqual(evalString('(1+3) * 2'), evalString('(1*2) + (3*2)')) 86 | -------------------------------------------------------------------------------- /pyparsing/ex5.py: -------------------------------------------------------------------------------- 1 | from pyparsing import Word, Literal, nums, ZeroOrMore, Forward 2 | import unittest 3 | 4 | """ 5 | op :: '+' | '-' | '*' | '/' 6 | integer :: '0'...'9'+ 7 | atom :: integer | '(' expr ')' 8 | term :: atom [mulop atom]* 9 | expr :: term [addop term]* 10 | """ 11 | 12 | exprStack = [] 13 | def pushFirst(s, l, t): 14 | exprStack.append(t[0]) 15 | 16 | add = Literal('+') 17 | sub = Literal('-') 18 | mul = Literal('*') 19 | div = Literal('/') 20 | lpar = Literal('(') 21 | rpar = Literal(')') 22 | addop = add | sub 23 | mulop = mul | div 24 | 25 | expr = Forward() 26 | atom = Word(nums).addParseAction(pushFirst) | (lpar + expr + rpar) 27 | term = atom + ZeroOrMore((mulop + atom).addParseAction(pushFirst)) 28 | expr << term + ZeroOrMore((addop + term).addParseAction(pushFirst)) 29 | 30 | def parseString(string): 31 | global exprStack 32 | exprStack = [] 33 | expr.parseString(string) 34 | return exprStack 35 | 36 | opf = { '+' : (lambda a, b: a + b), 37 | '-' : (lambda a, b: a - b), 38 | '*' : (lambda a, b: a * b), 39 | '/' : (lambda a, b: a / b) } 40 | 41 | def evalStack(stack): 42 | op = stack.pop() 43 | if op in '+-*/': 44 | op2 = evalStack(stack) 45 | op1 = evalStack(stack) 46 | return opf[op](op1, op2) 47 | else: 48 | return float(op) 49 | 50 | def evalString(string): 51 | stack = parseString(string) 52 | result = evalStack(stack) 53 | return result 54 | 55 | class TestEx2(unittest.TestCase): 56 | 57 | def test_parseString(self): 58 | self.assertEqual(parseString('6+3'), ['6', '3', '+']) 59 | self.assertEqual(parseString('6-3'), ['6', '3', '-']) 60 | self.assertEqual(parseString('6*3'), ['6', '3', '*']) 61 | self.assertEqual(parseString('6/3'), ['6', '3', '/']) 62 | 63 | def test_evalStack(self): 64 | self.assertEqual(evalStack(['6', '3', '+']), 9.0) 65 | self.assertEqual(evalStack(['6', '3', '-']), 3.0) 66 | self.assertEqual(evalStack(['6', '3', '*']), 18.0) 67 | self.assertEqual(evalStack(['6', '3', '/']), 2.0) 68 | 69 | def test_evalString(self): 70 | self.assertEqual(evalString('6+3'), 9.0) 71 | self.assertEqual(evalString('6-3'), 3.0) 72 | self.assertEqual(evalString('6*3'), 18.0) 73 | self.assertEqual(evalString('6/3'), 2.0) 74 | 75 | def test_multiple_op(self): 76 | self.assertEqual(evalString('6+3+2'), 11.0) 77 | self.assertEqual(evalString('6-3-2'), 1.0) 78 | self.assertEqual(evalString('6*3*2'), 36.0) 79 | self.assertEqual(evalString('6/3/2'), 1.0) 80 | 81 | def test_order_of_operations(self): 82 | self.assertEqual(evalString('2+3*4'), 14.0) 83 | self.assertEqual(evalString('5+4*3-2/1'), 15.0) 84 | 85 | def test_parentheses(self): 86 | self.assertEqual(evalString('(2+3)*4'), 20.0) 87 | self.assertEqual(evalString('(5+4)*((3-2)-1)'), 0.0) 88 | 89 | def test_commutative_property(self): 90 | self.assertEqual(evalString('3+4'), evalString('4+3')) 91 | self.assertEqual(evalString('2*5'), evalString('5*2')) 92 | 93 | def test_associative_property(self): 94 | self.assertEqual(evalString('(5+2) + 1'), evalString('5 + (2+1)')) 95 | self.assertEqual(evalString('(5*2) * 3'), evalString('5 * (2*3)')) 96 | 97 | def test_distributive_property(self): 98 | self.assertEqual(evalString('2 * (1+3)'), evalString('(2*1) + (2*3)')) 99 | -------------------------------------------------------------------------------- /env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | source ~/.profile 3 | source ~/.bashrc 4 | 5 | function info() { echo -e "\e[34m[INFO]\e[0m $1"; } 6 | 7 | function install_packages() { 8 | info "setup apt source" 9 | wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add - 10 | sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' 11 | 12 | sudo apt-get update 13 | 14 | info "install jenkins" 15 | sudo apt-get install jenkins 16 | 17 | info "install basic packages" 18 | sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm git 19 | 20 | info "install sqlite" 21 | sudo apt-get install -y sqlite3 libsqlite3-dev 22 | } 23 | 24 | function install_pyenv() { 25 | rm -rf ~/.pyenv 26 | 27 | info "install pyenv" 28 | git clone https://github.com/yyuu/pyenv.git ~/.pyenv 29 | 30 | info "setup environment" 31 | echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile 32 | echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile 33 | echo 'eval "$(pyenv init -)"' >> ~/.profile 34 | 35 | source ~/.profile 36 | } 37 | 38 | function install_python(){ 39 | info "install python 3.4.1" 40 | pyenv install 3.4.1 41 | pyenv versions 42 | 43 | info "switch python version" 44 | pyenv local 3.4.1 45 | pyenv version 46 | python --version 47 | } 48 | 49 | function install_pip() { 50 | info "install pip" 51 | wget https://bootstrap.pypa.io/get-pip.py 52 | python get-pip.py 53 | pip install -U pip 54 | } 55 | 56 | function install_pip_packages(){ 57 | info "install pip packages" 58 | pip install coverage nose pylint 59 | 60 | info "install behave" 61 | pip install behave 62 | pip install -U behave 63 | 64 | info "install django" 65 | pip install Django==1.9.7 66 | pip install behave-django 67 | pip install pyparsing 68 | } 69 | 70 | function install_virtualenv(){ 71 | info "install virtualenv" 72 | git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv 73 | pip install virtualenv 74 | 75 | info "setup virtualevn" 76 | mkdir -p ${HOME}/myWorkspace/venv 77 | 78 | virtualenv ${HOME}/myWorkspace/venv 79 | source ${HOME}/myWorkspace/venv/bin/activate 80 | 81 | echo "source ${HOME}/myWorkspace/venv/bin/activate" >> ~/.bashrc 82 | } 83 | 84 | function setup(){ 85 | case "$1" in 86 | apt-pkg) 87 | install_packages 88 | ;; 89 | pyenv) 90 | install_pyenv 91 | ;; 92 | python) 93 | install_python 94 | ;; 95 | pip) 96 | install_pip 97 | ;; 98 | pip-pkg) 99 | install_pip_packages 100 | ;; 101 | virtualenv) 102 | install_virtualenv 103 | ;; 104 | all) 105 | install_packages 106 | install_pyenv 107 | install_python 108 | install_pip 109 | install_pip_packages 110 | install_virtualenv 111 | ;; 112 | *) 113 | echo "Usage $0 apt-pkg|pyenv|python|pip|pip-pkg|virtualenv|all" 114 | exit 1 115 | esac 116 | } 117 | 118 | if [ $# == 0 ]; then setup help; fi 119 | for opt in $*; do setup $opt; done 120 | -------------------------------------------------------------------------------- /blog/blog/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for blog project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '5y(%6aa)9teeygp8cq#h=hut0dh6*u%6+_)du2v72nuxe#3_ru' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'grappelli', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'article', 42 | ] 43 | 44 | MIDDLEWARE_CLASSES = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'blog.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'blog.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'xd2n4@_fn)j=18vie4ush3u)0xmjbc3rsn@9*5o1+t_iix3hu-' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'behave_django', 35 | 'django_jenkins', 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 'calc', 43 | ] 44 | 45 | MIDDLEWARE_CLASSES = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'demo.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = 'demo.wsgi.application' 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.sqlite3', 83 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 109 | 110 | LANGUAGE_CODE = 'en-us' 111 | 112 | TIME_ZONE = 'UTC' 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | 126 | # Jenkins settings 127 | PROJECT_APPS = [ 128 | 'calc', 129 | ] 130 | 131 | JENKINS_TASKS = ( 132 | 'django_jenkins.tasks.run_pylint', 133 | ) 134 | -------------------------------------------------------------------------------- /test/features/steps/account.py: -------------------------------------------------------------------------------- 1 | import mysql.connector 2 | 3 | cnx = mysql.connector.connect(user='root', password='000000', host='127.0.0.1', database='test') 4 | cursor = cnx.cursor() 5 | 6 | def account_insert(username, password): 7 | query = "INSERT INTO account (username, password) VALUES ('%s', '%s')" % (username, password) 8 | cursor.execute(query) 9 | cnx.commit() 10 | 11 | def account_login(username, password): 12 | query = "SELECT id FROM account WHERE username='%s' AND password='%s'" % (username, password) 13 | cursor.execute(query) 14 | row = cursor.fetchone() 15 | return (row is not None) 16 | 17 | def account_register(username, password): 18 | if len(username) < 6 or len(password) < 6: 19 | return False 20 | account_insert(username, password) 21 | return True 22 | 23 | import unittest 24 | from unittest.mock import Mock, patch 25 | class TestAccount(unittest.TestCase): 26 | 27 | def setUp(self): 28 | self.result = None 29 | 30 | def test_account_insert(self): 31 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 32 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 33 | account_insert('abcdef', '123456') 34 | 35 | mock_execute.assert_called_with("INSERT INTO account (username, password) VALUES ('abcdef', '123456')") 36 | mock_commit.assert_called_with() 37 | 38 | def test_login_with_correct_username_password(self): 39 | def mock_execute(_, query): 40 | self.result = ('1',) if query == "SELECT id FROM account WHERE username='abcdef' AND password='123456'" else None 41 | def mock_fetchone(_): 42 | return self.result 43 | 44 | with patch('mysql.connector.cursor.MySQLCursor.execute', mock_execute): 45 | with patch('mysql.connector.cursor.MySQLCursor.fetchone', mock_fetchone): 46 | self.assertTrue(account_login('abcdef', '123456')) 47 | 48 | def test_login_with_invalid_username(self): 49 | def mock_execute(_, query): 50 | self.result = ('1',) if query == "SELECT id FROM account WHERE username='abcdef' AND password='123456'" else None 51 | def mock_fetchone(_): 52 | return self.result 53 | 54 | with patch('mysql.connector.cursor.MySQLCursor.execute', mock_execute): 55 | with patch('mysql.connector.cursor.MySQLCursor.fetchone', mock_fetchone): 56 | self.assertFalse(account_login('abc', '123456')) 57 | 58 | def test_login_with_invalid_password(self): 59 | def mock_execute(_, query): 60 | self.result = ('1',) if query == "SELECT id FROM account WHERE username='abcdef' AND password='123456'" else None 61 | def mock_fetchone(_): 62 | return self.result 63 | 64 | with patch('mysql.connector.cursor.MySQLCursor.execute', mock_execute): 65 | with patch('mysql.connector.cursor.MySQLCursor.fetchone', mock_fetchone): 66 | self.assertFalse(account_login('abcdef', '123')) 67 | 68 | def test_register_with_valid_username_password(self): 69 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 70 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 71 | self.assertTrue(account_register('abcdef', '123456')) 72 | 73 | self.assertTrue(mock_execute.called) 74 | self.assertTrue(mock_commit.called) 75 | 76 | def test_reigster_with_invalid_username(self): 77 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 78 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 79 | self.assertFalse(account_register('abc', '123456')) 80 | 81 | self.assertFalse(mock_execute.called) 82 | self.assertFalse(mock_commit.called) 83 | 84 | def test_register_with_invalid_password(self): 85 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 86 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 87 | self.assertFalse(account_register('abcdef', '123')) 88 | 89 | self.assertFalse(mock_execute.called) 90 | self.assertFalse(mock_commit.called) 91 | -------------------------------------------------------------------------------- /environment.md: -------------------------------------------------------------------------------- 1 | # 實驗環境 2 | 3 | ## 安裝虛擬機器 4 | 5 | - [VirtualBox](https://www.virtualbox.org/) 虛擬機器 6 | - [Vagrant](https://www.vagrantup.com/) 虛擬機器管理工具 7 | 8 | ## 下載作業系統影像檔 9 | 10 | 到 [Vagrantbox](http://www.vagrantbox.es/) 查詢適當的作業系統,我用 "debian jessie" 關鍵字查詢找到 "Debian Jessie 8.1.0 Release x64" 11 | 12 | ```shell 13 | $ vagrant box add https://atlas.hashicorp.com/ARTACK/boxes/debian-jessie 14 | $ vagrant box list 15 | ARTACK/debian-jessie (virtualbox, 8.1.0) 16 | debian7.8.0 (virtualbox, 0) 17 | opentable/win-2008r2-standard-amd64-nocm (virtualbox, 1.0.1) 18 | ubuntu/trusty64 (virtualbox, 20151020.0.0) 19 | ``` 20 | 21 | 初始化設定 22 | ```shell 23 | $ mkdir test 24 | $ cd test 25 | $ vagrant init ARTACK/debian-jessie 26 | ``` 27 | 28 | 因為將來要弄一個 web server,為了讓 host os 可以連接,需要產生一個 private network 介面,修改 Vagrantfile 加入下面一行 29 | ``` 30 | config.vm.network "private_network", ip: "192.168.33.10" 31 | ``` 32 | 33 | 啟動機器後登入系統 34 | ```shell 35 | $ vagrant up 36 | $ vagrant ssh 37 | ``` 38 | 39 | > 懶人安裝腳本:[env.sh](env.sh) 40 | 41 | ## 安裝基本套件 42 | 43 | ```shell 44 | $ sudo apt-get update 45 | $ sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm git 46 | ``` 47 | 48 | ## 安裝 pip - python 套件管理程式 49 | 50 | - [安裝 PIP 來管理 Python Packages](https://blog.longwin.com.tw/2014/08/python-setup-pip-package-2014/) 51 | - [pip](https://pip.pypa.io/en/stable/) 52 | 53 | ```shell 54 | $ sudo apt-get install python-pip 55 | ``` 56 | 57 | ## 安裝 pyenv、virtualenv 58 | 59 | 安裝與管理套件版本是軟體開發頗讓人頭疼的一環,幸好靠著 pyenv 管理多個 Python 版本問題,與 virtualenv 創造虛擬(獨立)Python 環境的工具,Python 工程師的生活才能過得輕鬆點。 60 | 61 | - [使用 Pyenv 管理多個 Python 版本](http://blog.codylab.com/python-pyenv-management/) 62 | - [pyenv 教程](https://wp-lai.gitbooks.io/learn-python/content/0MOOC/pyenv.html) 63 | - [Python 開發好幫手 – virtualenv](http://tech.mozilla.com.tw/posts/2155/python-%E9%96%8B%E7%99%BC%E5%A5%BD%E5%B9%AB%E6%89%8B-virtualenv) 64 | 65 | ```shell 66 | $ git clone https://github.com/yyuu/pyenv.git ~/.pyenv 67 | $ git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv 68 | $ sudo pip install virtualenv 69 | ``` 70 | 71 | 添加以下內容到 `~/.bashrc` 72 | ``` 73 | export PYENV_ROOT="$HOME/.pyenv" 74 | export PATH="$PYENV_ROOT/bin:$PATH" 75 | eval "$(pyenv init -)" 76 | ``` 77 | 78 | 重新載入 `~/.bashrc` 79 | ```shell 80 | $ source ~/.bashrc 81 | ``` 82 | 83 | 查看可用的 python 版本 84 | ```shell 85 | $ pyenv versions 86 | system 87 | ``` 88 | 89 | 列出可用的 python 版本 90 | ```shell 91 | $ pyenv install -l 92 | Available versions: 93 | ... 94 | 3.4.0 95 | 3.4-dev 96 | 3.4.1 97 | 3.4.2 98 | 3.4.3 99 | 3.4.4 100 | 3.5.0 101 | 3.5-dev 102 | 3.5.1 103 | 3.6.0a1 104 | 3.6-dev 105 | ... 106 | ``` 107 | 108 | 安裝 python 3.4.1 109 | ```shell 110 | $ pyenv install 3.4.1 111 | $ pyenv versions 112 | system 113 | * 3.4.1 (set by /home/vagrant/.python-version) 114 | ``` 115 | 116 | 切換 python 版本 117 | ```shell 118 | $ pyenv version 119 | system (set by /home/vagrant/.python-version) 120 | $ python --version 121 | Python 2.7.9 122 | 123 | $ pyenv local 3.4.1 124 | 125 | $ pyenv version 126 | 3.4.1 (set by /home/vagrant/.python-version) 127 | $ python --version 128 | Python 3.4.1 129 | ``` 130 | 131 | > Python 自 2.1 起開始內建 unittest,積極面可作為測試驅動開發(TDD),消極面至少提供一個容易上手的測試框架。往後實驗會用到 unittest.mock,這個在 python 2 沒有提供,需要切換到 python 3 才能使用測試替身 (Test Double) 切開相依的元件。 132 | 133 | 建立虛擬環境 134 | ```shell 135 | $ virtualenv .env 136 | $ source .env/bin/activate 137 | ``` 138 | 139 | 使用 pip 安裝套件,會安裝在 .env 140 | ```shell 141 | $ pip install coverage nose pylint 142 | ``` 143 | 144 | 修改 .bash,加入下面內容 (jenkins 登入後使用 .env) 145 | ``` 146 | . $HOME/.env/bin/activate 147 | ``` 148 | 149 | ## 安裝 behave - Python BDD Framework 150 | 151 | ```shell 152 | $ pip --version 153 | pip 1.5.6 from /home/vagrant/.pyenv/versions/3.4.1/lib/python3.4/site-packages (python 3.4) 154 | $ pip install behave 155 | pip install -U behave 156 | ``` 157 | 158 | ## 安裝 Sqlite - Lightweight SQL Database 159 | 160 | ```shell 161 | $ sudo apt-get install sqlite3 libsqlite3-dev 162 | ``` 163 | 164 | ## 安裝 Django - Python Web Framework 165 | 166 | - [How to get Django](https://www.djangoproject.com/download/) 167 | 168 | ```shell 169 | $ pip install Django==1.9.7 170 | ``` 171 | -------------------------------------------------------------------------------- /pyparsing_exercise.md: -------------------------------------------------------------------------------- 1 | # pyparsing 練習 2 | 3 | 這份文件不是 pyparsing 的完整說明,只為了把字串解析成數學運算式所做的練習與摸索。 4 | 5 | 以下透過一些練習,嘗試理解 [fourFn.py](http://pyparsing.wikispaces.com/file/view/fourFn.py) 這份解析工程數學運算式的程式碼。 6 | 7 | ## 練習一:熟悉 `Forward()`, `<<`, `setParseAction()`, `Suppress()` 8 | 9 | 完整程式碼: [ex1.py](pyparsing/ex1.py) 10 | ```python 11 | intStack = [] 12 | def pushStack(s, l, t): 13 | intStack.append(t[0]) 14 | 15 | atom = Word(nums).setParseAction(pushStack) | Suppress(Word(alphas)) 16 | expr = Forward() 17 | expr << atom + ZeroOrMore(expr) 18 | ``` 19 | - 解析字串,取出數字放入 `intStack`,忽略字母 20 | 21 | ## 練習二:簡單算數: 解析數字與運算符號 22 | 23 | 完整程式碼: [ex2.py](pyparsing/ex2.py) 24 | ```python 25 | """ 26 | op :: '+' | '-' | '*' | '/' 27 | num :: '0'...'9'+ 28 | expr :: num + [op + num]* 29 | """ 30 | 31 | exprStack = [] 32 | def pushFirst(s, l, t): 33 | exprStack.append(t[0]) 34 | 35 | add = Literal('+') 36 | sub = Literal('-') 37 | mul = Literal('*') 38 | div = Literal('/') 39 | op = add | sub | mul | div 40 | 41 | atom = Word(nums).addParseAction(pushFirst) 42 | expr = atom + ZeroOrMore((op + atom).addParseAction(pushFirst)) 43 | ``` 44 | - 滿足簡單加減乘除運算,但運算符號沒有次序性,所以只能解析如: '1+2+3-4', '1*2*3/4',無法處理加減乘除混合運算 45 | 46 | ```python 47 | def parseString(string): 48 | global exprStack 49 | exprStack = [] 50 | expr.parseString(string) 51 | return exprStack 52 | ``` 53 | - 將字串轉成運算的 stack,如 '1+2' => ['1', '2', '+'] 54 | 55 | ```python 56 | opf = { '+' : (lambda a, b: a + b), 57 | '-' : (lambda a, b: a - b), 58 | '*' : (lambda a, b: a * b), 59 | '/' : (lambda a, b: a / b) } 60 | 61 | def evalStack(stack): 62 | op = stack.pop() 63 | if op in '+-*/': 64 | op2 = evalStack(stack) 65 | op1 = evalStack(stack) 66 | return opf[op](op1, op2) 67 | else: 68 | return float(op) 69 | ``` 70 | - 處理 exprStack,取出元素如果是 71 | - 運算元(numbers),轉成浮點數 (可能當作除法除數或被除數),回傳結果 72 | - 運算子(+,-,*,/) 後再取出兩個運算元,呼叫運算子函數,傳入剛剛得到的運算元,回傳運算結果 73 | 74 | ```python 75 | def evalString(string): 76 | stack = parseString(string) 77 | result = evalStack(stack) 78 | return result 79 | ``` 80 | - 結合 `parseString()` 與 `evalStack()`,求出輸入字串的運算結果 81 | 82 | ## 練習三:符合先乘除、後加減 83 | 84 | 完整程式碼: [ex3.py](pyparsing/ex3.py) 85 | ```python 86 | """ 87 | op :: '+' | '-' | '*' | '/' 88 | atom :: '0'...'9'+ 89 | term :: atom + [mulop + atom]* 90 | expr :: term + [addop + term]* 91 | """ 92 | 93 | exprStack = [] 94 | def pushFirst(s, l, t): 95 | exprStack.append(t[0]) 96 | 97 | add = Literal('+') 98 | sub = Literal('-') 99 | mul = Literal('*') 100 | div = Literal('/') 101 | addop = add | sub 102 | mulop = mul | div 103 | 104 | atom = Word(nums).addParseAction(pushFirst) 105 | term = atom + ZeroOrMore((mulop + atom).addParseAction(pushFirst)) 106 | expr = term + ZeroOrMore((addop + term).addParseAction(pushFirst)) 107 | ``` 108 | - `expr` 先處理 `term`,再處理加減運算 109 | - `term` 先處理 `atom`,再處理乘除運算 110 | 111 | ## 練習四:括號優先權大於加減乘除 112 | 113 | 完整程式碼: [ex4.py](pyparsing/ex4.py) 114 | ```python 115 | """ 116 | op :: '+' | '-' | '*' | '/' 117 | integer :: '0'...'9'+ 118 | atom :: integer | '(' expr ')' 119 | term :: atom [mulop atom]* 120 | expr :: term [addop term]* 121 | """ 122 | 123 | exprStack = [] 124 | def pushFirst(s, l, t): 125 | exprStack.append(t[0]) 126 | 127 | add = Literal('+') 128 | sub = Literal('-') 129 | mul = Literal('*') 130 | div = Literal('/') 131 | lpar = Literal('(') 132 | rpar = Literal(')') 133 | addop = add | sub 134 | mulop = mul | div 135 | 136 | expr = Forward() 137 | atom = Word(nums).addParseAction(pushFirst) | (lpar + expr + rpar) 138 | term = atom + ZeroOrMore((mulop + atom).addParseAction(pushFirst)) 139 | expr << term + ZeroOrMore((addop + term).addParseAction(pushFirst)) 140 | ``` 141 | - 最基本的運算單位可能是"整數"或是"有括號的運算式" 142 | - 如果是整數,處理後推入 exprStack 143 | - 如果是括號運算式,進行遞迴求出括號裡面的值 144 | 145 | ## 練習五:驗證四則運算規則 146 | 147 | 完整程式碼: [ex5.py](pyparsing/ex5.py) 148 | ```python 149 | def test_order_of_operations(self): 150 | self.assertEqual(evalString('2+3*4'), 14.0) 151 | self.assertEqual(evalString('5+4*3-2/1'), 15.0) 152 | 153 | def test_parentheses(self): 154 | self.assertEqual(evalString('(2+3)*4'), 20.0) 155 | self.assertEqual(evalString('(5+4)*((3-2)-1)'), 0.0) 156 | 157 | def test_commutative_property` 158 | `(self): 159 | self.assertEqual(evalString('3+4'), evalString('4+3')) 160 | self.assertEqual(evalString('2*5'), evalString('5*2')) 161 | 162 | def test_associative_property(self): 163 | self.assertEqual(evalString('(5+2) + 1'), evalString('5 + (2+1)')) 164 | self.assertEqual(evalString('(5*2) * 3'), evalString('5 * (2*3)')) 165 | 166 | def test_distributive_property(self): 167 | self.assertEqual(evalString('2 * (1+3)'), evalString('(2*1) + (2*3)')) 168 | ``` 169 | - `test_order_of_operations` 驗證[運算次序](https://zh.wikipedia.org/wiki/%E9%81%8B%E7%AE%97%E6%AC%A1%E5%BA%8F) 170 | - `test_parentheses` 驗證括號優先權 171 | - `test_commutative_property` 驗證[交換律](https://zh.wikipedia.org/wiki/%E4%BA%A4%E6%8F%9B%E5%BE%8B) 172 | - `test_associative_property` 驗證[結合律](https://zh.wikipedia.org/wiki/%E7%BB%93%E5%90%88%E5%BE%8B) 173 | - `test_distributive_property` 驗證[分配律](https://zh.wikipedia.org/wiki/%E5%88%86%E9%85%8D%E5%BE%8B) 174 | -------------------------------------------------------------------------------- /sqlite.md: -------------------------------------------------------------------------------- 1 | # sqlite 快速瀏覽 2 | 3 | 這份文件快速瀏覽 SQLite 基本語法,包含創建資料庫、創建表格,新增、更新、查詢、刪除記錄 (CURD)。 4 | 5 | ## 參考資料 6 | 7 | - [SQLite 教程](http://www.runoob.com/sqlite/sqlite-tutorial.html) 8 | - [SQLite 官網](https://www.sqlite.org/) 9 | 10 | ## 創建資料庫 11 | - http://www.runoob.com/sqlite/sqlite-create-database.html 12 | 13 | 在當前目錄產生一個 testDB.db 檔案作為 SQLite 資料庫。 14 | ```shell 15 | $ sqlite3 testDB.db 16 | SQLite version 3.8.7.1 2014-10-29 13:59:56 17 | Enter ".help" for usage hints. 18 | sqlite> 19 | ``` 20 | 21 | 使用 `.databases` 命令列出資料庫的檔案名稱 22 | ```sql 23 | sqlite> .database 24 | seq name file 25 | --- --------------- ---------------------------------------------------------- 26 | 0 main /tmp/sqlite/testDB.db 27 | ``` 28 | 29 | 使用 `.quit` 退出資料庫 30 | ```sql 31 | sqlite> .quit 32 | ``` 33 | 34 | ## 創建表格 35 | - http://www.runoob.com/sqlite/sqlite-create-table.html 36 | 37 | 建立 `COMPANY` 與 `DEPARTMENT` 兩個表格 38 | ```sql 39 | sqlite> CREATE TABLE COMPANY ( 40 | ...> ID INT PRIMARY KEY NOT NULL, 41 | ...> NAME TEXT NOT NULL, 42 | ...> AGE INT NOT NULL, 43 | ...> ADDRESS CHAR(50), 44 | ...> SALARY REAL 45 | ...> ); 46 | sqlite> CREATE TABLE DEPARTMENT ( 47 | ...> ID INT PRIMARY KEY NOT NULL, 48 | ...> DEPT CHAR(50) NOT NULL, 49 | ...> EMP_ID INT NOT NULL 50 | ...> ); 51 | ``` 52 | 53 | 使用 `.tables` 列出表格 54 | ```sql 55 | sqlite> .tables 56 | COMPANY DEPARTMENT 57 | ``` 58 | 59 | 使用 `.schema` 顯示表格結構 60 | ```sql 61 | sqlite> .schema COMPANY 62 | CREATE TABLE COMPANY ( 63 | ID INT PRIMARY KEY NOT NULL, 64 | NAME TEXT NOT NULL, 65 | AGE INT NOT NULL, 66 | ADDRESS CHAR(50), 67 | SALARY REAL 68 | ); 69 | ``` 70 | 71 | ## 插入紀錄 72 | - http://www.runoob.com/sqlite/sqlite-insert.html 73 | 74 | 方法一 75 | ```sql 76 | sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) 77 | ...> VALUES (1, 'Paul', 32, 'California', 20000.00 ); 78 | sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) 79 | ...> VALUES (2, 'Allen', 25, 'Texas', 15000.00 ); 80 | sqlite> 81 | sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) 82 | ...> VALUES (3, 'Teddy', 23, 'Norway', 20000.00 ); 83 | sqlite> 84 | sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) 85 | ...> VALUES (4, 'Mark', 25, 'Rich-Mond ', 65000.00 ); 86 | sqlite> 87 | sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) 88 | ...> VALUES (5, 'David', 27, 'Texas', 85000.00 ); 89 | sqlite> 90 | sqlite> INSERT INTO COMPANY (ID,NAME,AGE,ADDRESS,SALARY) 91 | ...> VALUES (6, 'Kim', 22, 'South-Hall', 45000.00 ); 92 | ``` 93 | 94 | 方法二 95 | ```sql 96 | sqlite> INSERT INTO COMPANY VALUES (7, 'James', 24, 'Houston', 10000.00 ); 97 | ``` 98 | 99 | ## 查詢記錄 100 | - http://www.runoob.com/sqlite/sqlite-select.html 101 | 102 | 使用 `SELECT` 查詢表格資料 103 | ```sql 104 | sqlite> SELECT * FROM COMPANY; 105 | 1|Paul|32|California|20000.0 106 | 2|Allen|25|Texas|15000.0 107 | 3|Teddy|23|Norway|20000.0 108 | 4|Mark|25|Rich-Mond |65000.0 109 | 5|David|27|Texas|85000.0 110 | 6|Kim|22|South-Hall|45000.0 111 | 7|James|24|Houston|10000.0 112 | ``` 113 | 114 | 使用 `.header on` 命令顯示表格標頭,使用 `.mode column` 排列欄位,提高表格可讀性 115 | ```sql 116 | sqlite> .header on 117 | sqlite> .mode column 118 | sqlite> SELECT * FROM COMPANY; 119 | ID NAME AGE ADDRESS SALARY 120 | ---------- ---------- ---------- ---------- ---------- 121 | 1 Paul 32 California 20000.0 122 | 2 Allen 25 Texas 15000.0 123 | 3 Teddy 23 Norway 20000.0 124 | 4 Mark 25 Rich-Mond 65000.0 125 | 5 David 27 Texas 85000.0 126 | 6 Kim 22 South-Hall 45000.0 127 | 7 James 24 Houston 10000.0 128 | ``` 129 | 130 | ## 更新紀錄 131 | - http://www.runoob.com/sqlite/sqlite-update.html 132 | 133 | 使用 `UPDATE` 更新紀錄內容,用 `WHERE` 找出更新對象 134 | ```sql 135 | sqlite> UPDATE COMPANY SET ADDRESS = 'Texas' WHERE ID = 6; 136 | sqlite> SELECT * FROM COMPANY; 137 | ID NAME AGE ADDRESS SALARY 138 | ---------- ---------- ---------- ---------- ---------- 139 | 1 Paul 32 California 20000.0 140 | 2 Allen 25 Texas 15000.0 141 | 3 Teddy 23 Norway 20000.0 142 | 4 Mark 25 Rich-Mond 65000.0 143 | 5 David 27 Texas 85000.0 144 | 6 Kim 22 Texas 45000.0 145 | 7 James 24 Houston 10000.0 146 | ``` 147 | 148 | ## 刪除記錄 149 | - http://www.runoob.com/sqlite/sqlite-delete.html 150 | 151 | 使用 `DELETE` 刪除紀錄,用 `WHERE` 找出刪除對象 152 | ```sql 153 | sqlite> DELETE FROM COMPANY WHERE ID = 7; 154 | sqlite> SELECT * FROM COMPANY; 155 | ID NAME AGE ADDRESS SALARY 156 | ---------- ---------- ---------- ---------- ---------- 157 | 1 Paul 32 California 20000.0 158 | 2 Allen 25 Texas 15000.0 159 | 3 Teddy 23 Norway 20000.0 160 | 4 Mark 25 Rich-Mond 65000.0 161 | 5 David 27 Texas 85000.0 162 | 6 Kim 22 Texas 45000.0 163 | ``` 164 | 165 | ## 刪除表格 166 | - http://www.runoob.com/sqlite/sqlite-drop-table.html 167 | 168 | 刪除表格前,列出目前的表格 169 | ```sql 170 | sqlite> .tables 171 | COMPANY DEPARTMENT 172 | ``` 173 | 174 | 使用 `DROP TABLE` 刪除表格 175 | ```sql 176 | sqlite> DROP TABLE DEPARTMENT; 177 | sqlite> .tables 178 | COMPANY 179 | ``` 180 | -------------------------------------------------------------------------------- /unittest.md: -------------------------------------------------------------------------------- 1 | # unittest 筆記 2 | 3 | Python unittest 測試框架發想自 JUnit 並與其他單元測試的框架有類似的味道: 4 | 5 | - 支援自動化測試 6 | - 測試間共享 setup 與 shutdown 程式碼 7 | - 每次測試皆是獨立,結果不會受其他測試影響 8 | 9 | | 名詞 | 解釋 | 10 | |------|------| 11 | | 測試夾具 (test fixture) | 包含一個或多個被執行的測試,與相關的初始與結束動作。 | 12 | | 測試案例 (test case) | 個別單元測試,用來檢查特定輸入的反應。 | 13 | | 測試套組 (test suite) | 由測試案例、測試套組構成,用來集合應該被一起執行的測試。 | 14 | | 測試執行 (test runner) | 負責執行測試與回報結果。 | 15 | 16 | ## 基本範例 17 | 18 | Arithmetic.py: 19 | ```python 20 | import unittest 21 | 22 | def add(a, b): 23 | return a + b 24 | 25 | def subtract(a, b): 26 | return a - b 27 | 28 | def multiply(a, b): 29 | return a * b 30 | 31 | def divide(a, b): 32 | return a / b 33 | 34 | class TestArithmetic(unittest.TestCase): 35 | 36 | def test_add(self): 37 | self.assertEqual(add(1, 1), 2) 38 | 39 | def test_subtract(self): 40 | self.assertEqual(subtract(5, 2), 3) 41 | 42 | def test_multiply(self): 43 | self.assertEqual(multiply(3, 2), 6) 44 | 45 | def test_divide(self): 46 | self.assertEqual(divide(3.0, 2), 1.5) 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | ``` 51 | 52 | - `test_add` 測試 `add()` 函數是否正確運作 53 | - `test_subtract` 測試 `subtract()` 函數是否正確運作 54 | - `test_multiply` 測試 `multiply()` 函數是否正確運作 55 | - `test_divide` 測試 `divide()` 函數是否正確運作 56 | - `if __name__ == '__main__'` 判斷是否執行執行,若是則執行測試 57 | 58 | 執行結果: 59 | ```shell 60 | $ python Arithmetic.py 61 | .... 62 | ---------------------------------------------------------------------- 63 | Ran 4 tests in 0.000s 64 | 65 | OK 66 | /tmp/test$ python Arithmetic.py -v # 使用參數 -v 提供測試細節 67 | test_add (__main__.TestArithmetic) ... ok 68 | test_divide (__main__.TestArithmetic) ... ok 69 | test_multiply (__main__.TestArithmetic) ... ok 70 | test_subtract (__main__.TestArithmetic) ... ok 71 | 72 | ---------------------------------------------------------------------- 73 | Ran 4 tests in 0.000s 74 | 75 | OK 76 | ``` 77 | 78 | ## 命令列 79 | 80 | TestArithmetic.py: 81 | ```python 82 | import unittest 83 | 84 | class TestArithmetic(unittest.TestCase): 85 | 86 | def test_add(self): 87 | self.assertEqual(1 + 1, 2) 88 | 89 | def test_subtrat(self): 90 | self.assertEqual(5 - 2, 3) 91 | 92 | def test_multiply(self): 93 | self.assertEqual(2 * 3, 6) 94 | 95 | def test_divide(self): 96 | self.assertEqual(3.0 / 2, 1.5) 97 | ``` 98 | 99 | 執行結果: 100 | ```shell 101 | $ python -m unittest -h # 顯示 help message 102 | $ python -m unittest TestArithmetic # 測試 TestArithmetic 模組,不含副檔名 (.py) 103 | .... 104 | ---------------------------------------------------------------------- 105 | Ran 4 tests in 0.000s 106 | 107 | OK 108 | $ python -m unittest TestArithmetic.TestArithmetic # 測試 TestArithmetic 模組的 TestArithmetic 類別 109 | .... 110 | ---------------------------------------------------------------------- 111 | Ran 4 tests in 0.000s 112 | 113 | OK 114 | $ python -m unittest TestArithmetic.TestArithmetic.test_add # 測試 TestArithmetic 模組的 TestArithmetic 類別的 test_add 測試項目 115 | . 116 | ---------------------------------------------------------------------- 117 | Ran 1 test in 0.000s 118 | 119 | OK 120 | $ python -m unittest -v TestArithmetic # 顯示測試細節 121 | test_add (TestArithmetic.TestArithmetic) ... ok 122 | test_divide (TestArithmetic.TestArithmetic) ... ok 123 | test_multiply (TestArithmetic.TestArithmetic) ... ok 124 | test_subtrat (TestArithmetic.TestArithmetic) ... ok 125 | 126 | ---------------------------------------------------------------------- 127 | Ran 4 tests in 0.000s 128 | 129 | OK 130 | $ python -m unittest TestArithmetic TestStringMethods # 測試兩個模組 131 | ....... 132 | ---------------------------------------------------------------------- 133 | Ran 7 tests in 0.000s 134 | 135 | OK 136 | ``` 137 | 138 | ## 測試探索 139 | 140 | ```shell 141 | $ ls 142 | TestArithmetic.py TestStringMethods.py 143 | $ python -m unittest discover -p "Test*.py" # 找出 "Test*.py" 的檔案測試 144 | ....... 145 | ---------------------------------------------------------------------- 146 | Ran 7 tests in 0.000s 147 | 148 | OK 149 | $ python -m unittest discover -v -p "Test*.py" # 找出 "Test*.py" 的檔案測試,並顯示測試細節 150 | test_add (TestArithmetic.TestArithmetic) ... ok 151 | test_divide (TestArithmetic.TestArithmetic) ... ok 152 | test_multiply (TestArithmetic.TestArithmetic) ... ok 153 | test_subtrat (TestArithmetic.TestArithmetic) ... ok 154 | test_isupper (TestStringMethods.TestStringMethods) ... ok 155 | test_split (TestStringMethods.TestStringMethods) ... ok 156 | test_upper (TestStringMethods.TestStringMethods) ... ok 157 | 158 | ---------------------------------------------------------------------- 159 | Ran 7 tests in 0.000s 160 | 161 | OK 162 | ``` 163 | 164 | > 不清楚文件提到的 test suite 怎麼弄,先 pass 165 | 166 | ## 重新使用舊測試 167 | 168 | (略) 169 | 170 | ## 忽略測試與預期的錯誤 171 | 172 | testSkip.py: 173 | ```python 174 | import unittest 175 | import sys 176 | 177 | __version__ = (1, 2) 178 | 179 | class TestSkipCase(unittest.TestCase): 180 | 181 | @unittest.skip("demonstrating skipping") 182 | def test_nothing(self): 183 | self.fail("shouldn't happen") 184 | 185 | @unittest.skipIf(__version__ < (1, 3), "not supported in this library version") 186 | def test_format(self): 187 | # Tests that work for only a certain version of the library. 188 | pass 189 | 190 | @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows") 191 | def test_windows_support(self): 192 | # windows specific testing code 193 | pass 194 | 195 | def test_catchExcept(self): 196 | try: 197 | 1 / 0 198 | except: 199 | raise unittest.SkipTest("skipping because ...") 200 | self.assertEqual(True) 201 | ``` 202 | 203 | testExpectedFailure.py: 204 | ```python 205 | import unittest 206 | 207 | class ExpectedFailureTestCase(unittest.TestCase): 208 | @unittest.expectedFailure 209 | def test_fail(self): 210 | self.assertEqual(1, 0, "broken") 211 | ``` 212 | 213 | 測試結果: 214 | ```shell 215 | $ python -m unittest discover -v -p "test*.py" 216 | test_fail (testExpectedFailure.ExpectedFailureTestCase) ... expected failure 217 | test_catchExcept (testSkip.TestSkipCase) ... skipped 'skipping because ...' 218 | test_format (testSkip.TestSkipCase) ... skipped 'not supported in this library version' 219 | test_nothing (testSkip.TestSkipCase) ... skipped 'demonstrating skipping' 220 | test_windows_support (testSkip.TestSkipCase) ... skipped 'requires Windows' 221 | 222 | ---------------------------------------------------------------------- 223 | Ran 5 tests in 0.001s 224 | 225 | OK (skipped=4, expected failures=1) 226 | ``` 227 | 228 | 以下修飾詞 (decorator) 用來忽略或預期失敗: 229 | 230 | | 修飾詞 | 說明 | 231 | |--------|------| 232 | | `@unittest.skip(reason)` | 無條件忽略測試。reason 說明為何要忽略測試 | 233 | | `@unittest.skipIf(condition, reason)` | 如果條件成立,則忽略測試 | 234 | | `@unittest.skipUnless(condition, reason)` | 除非條件成立,否則忽略測試 | 235 | | `@unittest.expectedFailure` | 標記測試為預期錯誤。執行時如果測試失敗,結果不算失敗 | 236 | | `exception unittest.SkipTest(reason)` | 產生例外,忽略測試 | 237 | 238 | ## 測試方法 239 | 240 | | 方法 | 檢查 | 241 | |------|------| 242 | | `assertEqual(a, b)` | a == b | 243 | | `assertNotEqual(a, b)` | a != b | 244 | | `assertTrue(x)` | bool(x) is True | 245 | | `assertFalse(x)` | bool(x) is False | 246 | | `assertIs(a, b)` | a is b | 247 | | `assertIsNot(a, b)` | a is not b | 248 | | `assertIsNone(x)` | x is None | 249 | | `assertIsNotNone(x)` | x is not None | 250 | | `assertIn(a, b)` | a in b | 251 | | `assertNotIn(a, b)` | a not in b | 252 | | `assertIsInstance(a, b)` | isinstance(a, b) | 253 | | `assertNotIsInstance(a, b)` | not isinstance(a, b) | 254 | 255 | ## Grouping tests 256 | 257 | `class unittest.TestSuite(tests=())` 258 | 259 | == TBC == 260 | 261 | ---- 262 | ## 參考 263 | 264 | - [unittest — Unit testing framework](https://docs.python.org/3/library/unittest.html) 265 | -------------------------------------------------------------------------------- /behave.md: -------------------------------------------------------------------------------- 1 | # Behave 2 | 3 | ## 透過範例快速上手 4 | 5 | 參考:[Python BDD自动化测试框架初探](http://lovesoo.org/python-bdd-exploration-of-the-automated-testing-framework.html),計算 fibonacci 數列 6 | 7 | 首先建立目錄結構 8 | ```shell 9 | $ mkdir fib 10 | $ cd fib 11 | $ mkdir -p features/steps 12 | ``` 13 | 14 | 描述 feature 的長相 15 | ```python 16 | # file:features/fib.feature 17 | Feature:Calc Fib 18 | In order to introduce Behave 19 | We calc fib as example 20 | 21 | Scenario: Calc fib number 22 | Given we have the number 10 23 | when we calc the fib 24 | then we get the fib number 55 25 | ``` 26 | 27 | 執行 beheve 28 | ```shell 29 | $ behave 30 | Feature: Calc Fib # features/fib.feature:2 31 | In order to introduce Behave 32 | We calc fib as example 33 | Scenario: Calc fib number # features/fib.feature:6 34 | Given we have the number 10 # None 35 | When we calc the fib # None 36 | Then we get the fib number 55 # None 37 | 38 | 39 | Failing scenarios: 40 | features/fib.feature:6 Calc fib number 41 | 42 | 0 features passed, 1 failed, 0 skipped 43 | 0 scenarios passed, 1 failed, 0 skipped 44 | 0 steps passed, 0 failed, 0 skipped, 3 undefined 45 | Took 0m0.000s 46 | 47 | You can implement step definitions for undefined steps with these snippets: 48 | 49 | @given(u'we have the number 10') 50 | def step_impl(context): 51 | raise NotImplementedError(u'STEP: Given we have the number 10') 52 | 53 | @when(u'we calc the fib') 54 | def step_impl(context): 55 | raise NotImplementedError(u'STEP: When we calc the fib') 56 | 57 | @then(u'we get the fib number 55') 58 | def step_impl(context): 59 | raise NotImplementedError(u'STEP: Then we get the fib number 55') 60 | ``` 61 | 62 | 把 behave 提供的 snippets 拿來改成 steps 63 | ```python 64 | # file:features/steps/step_fib.py 65 | # ---------------------------------------------------------------------------- 66 | # STEPS: 67 | # ---------------------------------------------------------------------------- 68 | @given(u'we have the number 10') 69 | def step_impl(context): 70 | raise NotImplementedError(u'STEP: Given we have the number 10') 71 | 72 | @when(u'we calc the fib') 73 | def step_impl(context): 74 | raise NotImplementedError(u'STEP: When we calc the fib') 75 | 76 | @then(u'we get the fib number 55') 77 | def step_impl(context): 78 | raise NotImplementedError(u'STEP: Then we get the fib number 55') 79 | ``` 80 | 81 | 再跑一次 behave 82 | ```shell 83 | $ behave 84 | Feature: Calc Fib # features/fib.feature:2 85 | In order to introduce Behave 86 | We calc fib as example 87 | Scenario: Calc fib number # features/fib.feature:6 88 | Given we have the number 10 # features/steps/step_fib.py:2 0.000s 89 | Traceback (most recent call last): 90 | File "/usr/local/lib/python2.6/dist-packages/behave/model.py", line 1456, in run 91 | match.run(runner.context) 92 | File "/usr/local/lib/python2.6/dist-packages/behave/model.py", line 1903, in run 93 | self.func(context, *args, **kwargs) 94 | File "features/steps/step_fib.py", line 4, in step_impl 95 | raise NotImplementedError(u'STEP: Given we have the number 10') 96 | NotImplementedError: STEP: Given we have the number 10 97 | 98 | When we calc the fib # None 99 | Then we get the fib number 55 # None 100 | 101 | 102 | Failing scenarios: 103 | features/fib.feature:6 Calc fib number 104 | 105 | 0 features passed, 1 failed, 0 skipped 106 | 0 scenarios passed, 1 failed, 0 skipped 107 | 0 steps passed, 1 failed, 2 skipped, 0 undefined 108 | Took 0m0.000s 109 | ``` 110 | 111 | 修改 steps,並實作 fibs 112 | ```python 113 | # file:features/steps/step_fib.py 114 | # ---------------------------------------------------------------------------- 115 | # PROBLEM DOMAIN: 116 | # ---------------------------------------------------------------------------- 117 | def fibs(num): 118 | a = b = 1 119 | for i in range(num): 120 | yield a 121 | a, b = b, a + b 122 | # ---------------------------------------------------------------------------- 123 | # STEPS: 124 | # ---------------------------------------------------------------------------- 125 | @given(u'we have the number 10') 126 | def step_impl(context): 127 | context.fib_number = 10 128 | 129 | @when(u'we calc the fib') 130 | def step_impl(context): 131 | context.fib_number=list(fibs(context.fib_number))[-1] 132 | 133 | @then(u'we get the fib number 55') 134 | def step_impl(context): 135 | context.expected_number = 55 136 | assert context.fib_number == context.expected_number, "Calc fib number: %d" % context.fib_number 137 | ``` 138 | - 實作出 `fibs` 139 | - 修改每個 `step_impl` 140 | 141 | > `list(fibs(context.fib_number))[-1]`: 將 `fibs()` 計算結果轉成 `list`,然侯取出最後一個值 (寫得很有技巧,但可讀性很差) 142 | 143 | 將 steps 改得更有彈性 144 | ```python 145 | # file:features/steps/step_fib.py 146 | # ---------------------------------------------------------------------------- 147 | # PROBLEM DOMAIN: 148 | # ---------------------------------------------------------------------------- 149 | def fibs(num): 150 | a = b = 1 151 | for i in range(num): 152 | yield a 153 | a, b = b, a + b 154 | # ---------------------------------------------------------------------------- 155 | # STEPS: 156 | # ---------------------------------------------------------------------------- 157 | @given(u'we have the number {number}') 158 | def step_impl(context, number): 159 | context.fib_number = int(number) 160 | 161 | @when(u'we calc the fib') 162 | def step_impl(context): 163 | context.fib_number=list(fibs(context.fib_number))[-1] 164 | 165 | @then(u'we get the fib number {number}') 166 | def step_impl(context, number): 167 | context.expected_number = int(number) 168 | assert context.fib_number == context.expected_number, "Calc fib number: %d" % context.fib_number 169 | ``` 170 | 171 | 跑一次 behave 172 | ```shell 173 | $ behave 174 | Feature: Calc Fib # features/fib.feature:2 175 | In order to introduce Behave 176 | We calc fib as example 177 | Scenario: Calc fib number # features/fib.feature:6 178 | Given we have the number 10 # features/steps/step_fib.py:13 0.000s 179 | When we calc the fib # features/steps/step_fib.py:17 0.000s 180 | Then we get the fib number 55 # features/steps/step_fib.py:21 0.000s 181 | 182 | 1 feature passed, 0 failed, 0 skipped 183 | 1 scenario passed, 0 failed, 0 skipped 184 | 3 steps passed, 0 failed, 0 skipped, 0 undefined 185 | Took 0m0.000s 186 | ``` 187 | 188 | 修改 features: 新增測試條件 189 | ```python 190 | # file:features/fib.feature 191 | Feature:Calc Fib 192 | In order to introduce Behave 193 | We calc fib as example 194 | 195 | Scenario Outline: Calc fib number 196 | Given we have the number 197 | When we calc the fib 198 | Then we get the fib number 199 | 200 | Examples: Some numbers 201 | | number | fib_number | 202 | | 1 | 1 | 203 | | 2 | 2 | 204 | | 10 | 55 | 205 | ``` 206 | 207 | 執行 behave 驗證 208 | ```shell 209 | $ behave 210 | Feature: Calc Fib # features/fib.feature:2 211 | In order to introduce Behave 212 | We calc fib as example 213 | Scenario Outline: Calc fib number -- @1.1 Some numbers # features/fib.feature:13 214 | Given we have the number 1 # features/steps/step_fib.py:13 0.000s 215 | When we calc the fib # features/steps/step_fib.py:17 0.000s 216 | Then we get the fib number 1 # features/steps/step_fib.py:21 0.000s 217 | 218 | Scenario Outline: Calc fib number -- @1.2 Some numbers # features/fib.feature:14 219 | Given we have the number 2 # features/steps/step_fib.py:13 0.000s 220 | When we calc the fib # features/steps/step_fib.py:17 0.000s 221 | Then we get the fib number 2 # features/steps/step_fib.py:21 0.000s 222 | Assertion Failed: Calc fib number: 1 223 | 224 | 225 | Scenario Outline: Calc fib number -- @1.3 Some numbers # features/fib.feature:15 226 | Given we have the number 10 # features/steps/step_fib.py:13 0.000s 227 | When we calc the fib # features/steps/step_fib.py:17 0.000s 228 | Then we get the fib number 55 # features/steps/step_fib.py:21 0.000s 229 | 230 | 231 | Failing scenarios: 232 | features/fib.feature:14 Calc fib number -- @1.2 Some numbers 233 | 234 | 0 features passed, 1 failed, 0 skipped 235 | 2 scenarios passed, 1 failed, 0 skipped 236 | 8 steps passed, 1 failed, 0 skipped, 0 undefined 237 | Took 0m0.001s 238 | ``` 239 | - 發生錯誤 `Assertion Failed: Calc fib number: 1`: 原因是 features 定義時誤以為第二個 fibonacci 數是 2 (1 才是對的) 240 | 241 | ---- 242 | ## 參考 243 | 244 | - [Behave official site](http://pythonhosted.org/behave/) 245 | - [behave Examples and Tutorials](https://jenisys.github.io/behave.example/index.html) 246 | - [Behavior-Driven Development in Python](http://code.tutsplus.com/tutorials/behavior-driven-development-in-python--net-26547) 247 | -------------------------------------------------------------------------------- /behave-django.md: -------------------------------------------------------------------------------- 1 | # behave-django 2 | 3 | ## 安裝 4 | 5 | behave-django 要安裝在 Django project 中,在這之前要先設定虛擬環境、安裝 Django、產生 Django project 6 | 7 | ```shell 8 | $ source venv/bin/activate 9 | $ pip install Django==1.9.7 10 | $ django-admin.py startproject mysite 11 | $ cd mysite/ 12 | $ pip install behave-django 13 | ``` 14 | 15 | 修改 mysite/settings.py,新增 behave_django app 16 | ```python 17 | INSTALLED_APPS = [ 18 | ... 19 | 'behave_django', 20 | ] 21 | ``` 22 | 23 | ### 一個簡單的範例 24 | 25 | 在 mysite/ 目錄裡產生以下目錄與檔案 26 | ``` 27 | features/ 28 | ├── environment.py 29 | ├── running-tests.feature 30 | └── steps 31 | └── running_tests.py 32 | ``` 33 | 34 | features/environment.py: 35 | ```python 36 | """ 37 | behave environment module for testing behave-django 38 | """ 39 | 40 | def before_feature(context, feature): 41 | if feature.name == 'Fixture loading': 42 | context.fixtures = ['behave-fixtures.json'] 43 | 44 | 45 | def before_scenario(context, scenario): 46 | if scenario.name == 'Load fixtures for this scenario and feature': 47 | context.fixtures.append('behave-second-fixture.json') 48 | ``` 49 | 50 | features/running-tests.feature: 51 | ```python 52 | Feature: Running tests 53 | In order to prove that behave-django works 54 | As the Maintainer 55 | I want to test running behave against this features directory 56 | 57 | Scenario: The Test 58 | Given this step exists 59 | When I run "python manage.py behave" 60 | Then I should see the behave tests run 61 | ``` 62 | 63 | features/steps/running_tests.py: 64 | ```python 65 | from behave import given, when, then 66 | 67 | @given(u'this step exists') 68 | def step_exists(context): 69 | pass 70 | 71 | @when(u'I run "python manage.py behave"') 72 | def run_command(context): 73 | pass 74 | 75 | @then(u'I should see the behave tests run') 76 | def is_running(context): 77 | pass 78 | ``` 79 | 80 | 執行測試 81 | ```shell 82 | $ python manage.py behave 83 | 84 | Creating test database for alias 'default'... 85 | Feature: Running tests # features/running-tests.feature:1 86 | In order to prove that behave-django works 87 | As the Maintainer 88 | I want to test running behave against this features directory 89 | Scenario: The Test # features/running-tests.feature:6 90 | Given this step exists # features/steps/running_tests.py:4 0.000s 91 | When I run "python manage.py behave" # features/steps/running_tests.py:9 0.000s 92 | Then I should see the behave tests run # features/steps/running_tests.py:14 0.000s 93 | 94 | 1 feature passed, 0 failed, 0 skipped 95 | 1 scenario passed, 0 failed, 0 skipped 96 | 3 steps passed, 0 failed, 0 skipped, 0 undefined 97 | Took 0m0.000s 98 | Destroying test database for alias 'default'... 99 | ``` 100 | 101 | > 附註:從 0.2.0 開始,不再需要在 environment.py 中插入 `environment.before_scenario()` 與 `environment.after_scenario()`。 102 | 103 | ### 下載範例 104 | 105 | ```shell 106 | $ git clone https://github.com/behave/behave-django 107 | ``` 108 | 109 | 以下,透過下載範例進行說明。 110 | 111 | ## 用法 112 | 113 | ### 網頁瀏覽器自動化 (Web browser automation) 114 | 115 | 網頁自動化函式庫可以透過 `context.base_url` 存取伺服器。此外,使用 `context.get_url()` 得到保留專案URL與絕對路徑。 116 | 117 | ```python 118 | # Get context.base_url 119 | context.get_url() 120 | 121 | # Get context.base_url + '/absolute/url/here' 122 | context.get_url('/absolute/url/here') 123 | 124 | # Get context.base_url + reverse('view-name') 125 | context.get_url('view-name') 126 | 127 | # Get context.base_url + reverse('view-name', 'with args', and='kwargs') 128 | context.get_url('view-name', 'with args', and='kwargs') 129 | 130 | # Get context.base_url + model_instance.get_absolute_url() 131 | context.get_url(model_instance) 132 | ``` 133 | 134 | #### 網頁自動化測試 135 | 136 | 透過瀏覽器打開 `http://192.168.33.10:8000/`,得到下面內容 137 | ``` 138 | Behave Django works 139 | ``` 140 | 141 | 以下驗證透過瀏覽器看到的內容合乎預期。 142 | 143 | features/live-test-server.feature 144 | ``` 145 | Feature: Live server 146 | In order to prove that the live server works 147 | As the Maintainer 148 | I want to send an HTTP request 149 | 150 | Scenario: HTTP GET 151 | When I visit "/" 152 | Then I should see "Behave Django works" 153 | ``` 154 | 155 | features/steps/live_test_server.py 156 | ```python 157 | @when(u'I visit "{url}"') 158 | def visit(context, url): 159 | page = urlopen(context.base_url + url) 160 | context.response = str(page.read()) 161 | 162 | @then(u'I should see "{text}"') 163 | def i_should_see(context, text): 164 | assert text in context.response 165 | ``` 166 | - 透過 `context.base_url` 取得網頁URL,使用 `urlopen()` 取得網頁物件 167 | - `context.response = str(page.read())` 讀取網頁內容 168 | - `assert text in context.response` 驗證讀取的網頁內容符合預期 169 | 170 | 執行結果 171 | ```shell 172 | $ python manage.py behave --include=live-test-server 173 | Creating test database for alias 'default'... 174 | Feature: Live server # features/live-test-server.feature:1 175 | In order to prove that the live server works 176 | As the Maintainer 177 | I want to send an HTTP request 178 | Scenario: HTTP GET # features/live-test-server.feature:6 179 | When I visit "/" # features/steps/live_test_server.py:9 0.008s 180 | Then I should see "Behave Django works" # features/steps/live_test_server.py:15 0.000s 181 | 182 | 1 feature passed, 0 failed, 0 skipped 183 | 1 scenario passed, 0 failed, 0 skipped 184 | 2 steps passed, 0 failed, 0 skipped, 0 undefined 185 | Took 0m0.009s 186 | Destroying test database for alias 'default'... 187 | ``` 188 | 189 | ### 測試用客戶端 (Django’s testing client) 190 | 191 | `context` 包含 TestCase 的實例。`context.test` 提供 Django 測試客戶端程式,驗證透過 URL 讀取網頁內容是否合乎預期。 192 | 193 | 修改 features/steps/live_test_server.py 194 | ```python 195 | @when(u'I visit "{url}"') 196 | def visit(context, url): 197 | context.response = context.test.client.get(url) 198 | 199 | @then(u'I should see "{text}"') 200 | def i_should_see(context, text): 201 | context.test.assertContains(context.response, text) 202 | ``` 203 | - `context.text` 提供一堆類似 unittest 的函式,例如 `assertRedirects`, `assertContains`, `assertNotContains`, `assertFormError`, `assertFormsetError`, `assertTemplateUsed`, `assertTemplateNotUsed`, `assertRaisesMessage`, `assertFieldOutput`, `assertHTMLEqual`, `assertHTMLNotEqual`, `assertInHTML`, `assertJSONEqual`, `assertJSONNotEqual`, `assertXMLEqual`, `assertXMLNotEqual`, `assertQuerysetEqual`, `assertNumQueries` 204 | 205 | ### 資料庫事務 (Database transactions per scenario) 206 | (看不懂) 207 | 208 | ### 載入基礎設施 (Fixture loading) 209 | 210 | 我的理解是,可以事先使用 json (例如,fixtures/behave-fixtures.json) 設定一些物件,在環境 (environment.py) 設定時載入成為 Model 的內容。 211 | 212 | features/environment.py 213 | ```python 214 | def before_feature(context, feature): 215 | if feature.name == 'Fixture loading': 216 | context.fixtures = ['behave-fixtures.json'] 217 | 218 | def before_scenario(context, scenario): 219 | if scenario.name == 'Load fixtures for this scenario and feature': 220 | context.fixtures.append('behave-second-fixture.json') 221 | ``` 222 | - 如果 feature 是 'Fixture loading',載入 'behave-fixtures.json' >> BehaveTestModel 內有一個物件 223 | - 如果 feature 是 'Load fixtures for this scenario and feature',附加 'behave-second-fixture.json' >> BehaveTestModel 內有兩個物件 224 | 225 | test_app/fixtures/behave-fixtures.json 226 | ```json 227 | [{ 228 | "fields": { 229 | "name": "fixture loading test", 230 | "number": 42 231 | }, 232 | "model": "test_app.behavetestmodel", 233 | "pk": 1 234 | }] 235 | ``` 236 | 237 | test_app/fixtures/behave-second-fixture.json 238 | ``` 239 | [{ 240 | "fields": { 241 | "name": "second fixture", 242 | "number": 7 243 | }, 244 | "model": "test_app.behavetestmodel", 245 | "pk": 2 246 | }] 247 | ``` 248 | 249 | test_app/models.py 250 | ```python 251 | class BehaveTestModel(models.Model): 252 | name = models.CharField(max_length=255) 253 | number = models.IntegerField() 254 | 255 | def get_absolute_url(self): 256 | return '/behave/test/%i/%s' % (self.number, self.name) 257 | ``` 258 | - `BehaveTestModel` 有兩個屬性: name, number 259 | 260 | features/fixture-loading.feature 261 | ``` 262 | Feature: Fixture loading 263 | In order to have sample data during my behave tests 264 | As the Maintainer 265 | I want to load fixtures 266 | 267 | Scenario: Load fixtures 268 | Then the fixture should be loaded 269 | 270 | Scenario: Load fixtures for this scenario and feature 271 | Then the fixture for the second scenario should be loaded 272 | ``` 273 | 274 | features/steps/fixture-loading.py 275 | ```python 276 | from test_app.models import BehaveTestModel 277 | 278 | @then(u'the fixture should be loaded') 279 | def check_fixtures(context): 280 | context.test.assertEqual(BehaveTestModel.objects.count(), 1) 281 | 282 | @then(u'the fixture for the second scenario should be loaded') 283 | def check_second_fixtures(context): 284 | context.test.assertEqual(BehaveTestModel.objects.count(), 2) 285 | ``` 286 | - 搭配 features/environment.py 閱讀,就能理解 assert 為何合乎預期 287 | 288 | ## 命令列選項 (Command line options) 289 | 290 | - `--use-existing-database`: 不產生測試用資料庫,使用 runserver 時使用的資料庫。(除非必須這樣做,不然不要冒險) 291 | - `--keepdb`: 測試使用已有的資料庫而不是每次都重新產生,得到比較快的測試速度。(Django 1.8 之後已經把 --keepdb 加到 manage.py 了) 292 | 293 | ## Behave 配置檔 (Behave configuration file) 294 | 295 | 專案根目錄下的 behave.ini, .behaverc, or setup.cfg 檔案。 296 | 297 | 例如,setup.cfg 298 | ``` 299 | [behave] 300 | paths = features/ 301 | test_app/features/ 302 | show_skipped = no 303 | 304 | [flake8] 305 | exclude = docs/* 306 | 307 | [pytest] 308 | testpaths = tests/ 309 | ``` 310 | 311 | 更多資料在 [Behave Configuration Files](https://pythonhosted.org/behave/behave.html#configuration-files) 312 | 313 | ---- 314 | ## 參考 315 | 316 | - https://pythonhosted.org/behave-django/index.html (文件) 317 | - https://github.com/behave/behave-django/tree/master/features (範例) 318 | -------------------------------------------------------------------------------- /pyparsing.md: -------------------------------------------------------------------------------- 1 | # pyparsing 2 | 3 | 除了 [Getting Started with Pyparsing](http://shop.oreilly.com/product/9780596514235.do) 這本書,pyparsing 似乎沒有完整的說明文件?!以下內容來自 [pyparsing quick reference](http://infohost.nmt.edu/tcc/help/pubs/pyparsing/web/index.html)。 4 | 5 | ## pyparsing: 從本文抽取訊息的工具 6 | 7 | pyparsing 模組提供程式設計師使用 python 語言從結構化的文本資料抽取資訊。 8 | 9 | 這個工具比正規化表示式更強大 (python re 模組),但又不像編譯器那樣一般化。 10 | 11 | 為了找出結構化文本中的訊息,我們必須描述結構。pyparsing 模組建立在 [Backus-Naur Form](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form) (BNF) 語法描述技術的基礎上。熟悉 BNF 的語法標記有助於使用 pyparsing。 12 | 13 | pyparsing 模組運作的方式是使用遞迴減少解析器 [recursive descent parser](https://en.wikipedia.org/wiki/Recursive_descent_parser) 匹配輸入文字:我們寫出像 BNF 語法產物,然後 pyparsing 提供機制用這些產物匹配輸入文本。 14 | 15 | The pyparsing module works best when you can describe the exact syntactic structure of the text you are analyzing. A common application of pyparsing is the analysis of log files. Log file entries generally have a predictable structure including such fields as dates, IP addresses, and such. Possible applications of the module to natural language work are not addressed here. 16 | 17 | 當你能精準描述分析的文本結構,pyparsing 模組就能發揮強大的作用。通常 pyparsing 會拿來分析 log 檔。log 檔通常有個可預測的結構,欄位包含日期、IP位置、等等。 18 | 19 | ## 建構應用程式 20 | 21 | 1. 寫出 BNF 描述要分析文本的結構 22 | 2. 視需要,安裝 pyparsing 模組 23 | 3. 在 python script 中匯入 pyparsing 模組:`import pyparsing as pp` 24 | 4. 在 python script 中寫出能匹配 BNF 的 parser 25 | 5. 準備要分析的文本 26 | 6. 如果用 p 表示 parser,用 s 表示文本,執行的程式碼就像 `p.parseString(s)` 27 | 7. 從解析結果得到需要的訊息 28 | 29 | ## 一個小型完整的範例 30 | 31 | python 識別符號包含一個或多的字元,第一個字元是 字母或 `_`,接著可能是 字母、數字、或 `_`。用 BNF 方式寫成 32 | 33 | ``` 34 | first ::= letter | "_" 35 | letter ::= "a" | "b" | ... "z" | "A" | "B" | ... | "Z" 36 | digit ::= "0" | "1" | ... | "9" 37 | rest ::= first | digit 38 | identifier ::= first rest* 39 | ``` 40 | 41 | 最終產出物可讀作:一個識別符號包含一個 `first`,接著可能有零個或多個 `rest`。 42 | 43 | 以下程式碼 44 | ```python 45 | #!/usr/bin/env python 46 | #================================================================ 47 | # trivex: Trivial example 48 | #---------------------------------------------------------------- 49 | 50 | # - - - - - I m p o r t s 51 | 52 | import sys 53 | 54 | import pyparsing as pp 55 | 56 | # - - - - - M a n i f e s t c o n s t a n t s 57 | 58 | first = pp.Word(pp.alphas+"_", exact=1) 59 | rest = pp.Word(pp.alphanums+"_") 60 | identifier = first+pp.Optional(rest) 61 | 62 | testList = [ # List of test strings 63 | # Valid identifiers 64 | "a", "foo", "_", "Z04", "_bride_of_mothra", 65 | # Not valid 66 | "", "1", "$*", "a_#" ] 67 | 68 | # - - - - - m a i n 69 | 70 | def main(): 71 | """ 72 | """ 73 | for text in testList: 74 | test(text) 75 | 76 | # - - - t e s t 77 | 78 | def test(s): 79 | '''See if s matches identifier. 80 | ''' 81 | print ("---Test for '{0}'".format(s)) 82 | 83 | try: 84 | result = identifier.parseString(s) 85 | print " Matches: {0}".format(result) 86 | except pp.ParseException as x: 87 | print " No match: {0}".format(str(x)) 88 | 89 | # - - - - - E p i l o g u e 90 | 91 | if __name__ == "__main__": 92 | main() 93 | ``` 94 | ``` 95 | $ python trivex.py 96 | ---Test for 'a' 97 | Matches: ['a'] 98 | ---Test for 'foo' 99 | Matches: ['f', 'oo'] 100 | ---Test for '_' 101 | Matches: ['_'] 102 | ---Test for 'Z04' 103 | Matches: ['Z', '04'] 104 | ---Test for '_bride_of_mothra' 105 | Matches: ['_', 'bride_of_mothra'] 106 | ---Test for '' 107 | No match: Expected W:(ABCD...) (at char 0), (line:1, col:1) 108 | ---Test for '1' 109 | No match: Expected W:(ABCD...) (at char 0), (line:1, col:1) 110 | ---Test for '$*' 111 | No match: Expected W:(ABCD...) (at char 0), (line:1, col:1) 112 | ---Test for 'a_#' 113 | Matches: ['a', '_'] 114 | ``` 115 | 116 | 回傳值是 `pp.ParseResults class` 的實例。當列印出來,會像個 list。單一字母的測試字串只有一個元素,多字母字串有兩個元素,一個是 `first` 接著是 `rest`。 117 | 118 | 如果想讓回傳值合併匹配的元素,使用 119 | ```python 120 | identifier = pp.Combine(first+pp.Optional(rest)) 121 | ``` 122 | 123 | 結果會像這樣 124 | ``` 125 | ---Test for '_bride_of_mothra' 126 | Matches: ['_bride_of_mothra'] 127 | ``` 128 | 129 | ## 如何組織回傳結果 ParseResults 130 | 131 | 當輸入與建立的解析器匹配,`.parseString()` 回傳 `class ParseResults` 的實例。 132 | 133 | 對於一個複雜的結構,這個實例可能有很多訊息在裡面。`ParseResults` 實例的結構跟你如何建立解析器有關。 134 | 135 | 存取 `ParserResults` 有幾種方式: 136 | - 當作一個 list 137 | ```python 138 | >>> import pyparsing as pp 139 | >>> number = pp.Word(pp.nums) 140 | >>> result = number.parseString('17') 141 | >>> print result 142 | ['17'] 143 | >>> type(result) 144 | 145 | >>> result[0] 146 | '17' 147 | >>> list(result) 148 | ['17'] 149 | >>> numberList = pp.OneOrMore(number) 150 | >>> print numberList.parseString('17 33 88') 151 | ['17', '33', '88'] 152 | ``` 153 | - 當作一個 dictionary 154 | ```python 155 | >>> number = pp.Word(pp.nums).setResultsName('nVache') 156 | >>> result = number.parseString('17') 157 | >>> print result 158 | ['17'] 159 | >>> result['nVache'] 160 | '17' 161 | ``` 162 | 163 | ### 使用 `pp.Group()` 分而治之 164 | 165 | 如果採取分而治之的原則 (分段精練),解析器的組織會更容易追蹤理解。 166 | 167 | 實例上,這個意指頂層的 `ParseResults` 應該包含更多子部分。 168 | 如果這一層有太多子部分,查看輸入把它拆解成兩個或更多的子解析器。 169 | 然後組織頂層,它只會包含這些。 170 | 如果需要,拆解小解析器成為更小的,直到每個解析器都能用內建的基礎功能清楚定義。 171 | 172 | ---- 173 | ## `Word`: Match characters from a specified set 174 | 175 | ```python 176 | from pyparsing import Word 177 | from pyparsing import nums, alphas, alphanums 178 | 179 | name = Word('abcdef') 180 | print(name.parseString('fadedglory')) 181 | 182 | pyName = Word(alphas + '_', bodyChars = alphanums + '_') 183 | print(pyName.parseString('_crunchyFrog13')) 184 | 185 | name4 = Word(alphas, exact=4) 186 | print(name4.parseString('Whizzo')) 187 | 188 | noXY = Word(alphas, excludeChars='xy') 189 | print(noXY.parseString('Sussex')) 190 | ``` 191 | ``` 192 | ['faded'] 193 | ['_crunchyFrog13'] 194 | ['Whiz'] 195 | ['Susse'] 196 | ``` 197 | ## `ZeroOrMore`: Match any number of repetitions including none 198 | 199 | ```python 200 | from pyparsing import Word, ZeroOrMore 201 | from pyparsing import nums, alphas 202 | 203 | item = Word(nums) | Word(alphas) 204 | expr = ZeroOrMore(item) 205 | bnf = expr 206 | 207 | tests = ("123abc456def", "123 abc 456 def", "abc def 123 456") 208 | 209 | for t in tests: 210 | print(t, " >>> ", bnf.parseString(t)) 211 | ``` 212 | ``` 213 | 123abc456def >>> ['123', 'abc', '456', 'def'] 214 | 123 abc 456 def >>> ['123', 'abc', '456', 'def'] 215 | abc def 123 456 >>> ['abc', 'def', '123', '456'] 216 | ``` 217 | 218 | ## `StringEnd`: Match the end of the text 219 | 220 | ```python 221 | >>> from pyparsing import Word, StringEnd, alphas 222 | >>> noEnd = Word(alphas) 223 | >>> noEnd.parseString('Dorking...') 224 | (['Dorking'], {}) 225 | >>> withEnd = Word(alphas) + StringEnd() 226 | >>> withEnd.parseString('Dorking...') 227 | Traceback (most recent call last): 228 | ...(skipped) 229 | pyparsing.ParseException: Expected end of text (at char 7), (line:1, col:8) 230 | ``` 231 | 232 | ## `Suppress`: Omit matched text from the result 233 | ```python 234 | from pyparsing import Word, Literal, Suppress 235 | from pyparsing import nums, alphas, alphanums 236 | 237 | name = Word(alphas) 238 | lb = Literal('[') 239 | rb = Literal(']') 240 | 241 | pat1 = lb + name + rb 242 | print(pat1.parseString('[Pewty]')) 243 | 244 | pat2 = Suppress(lb) + name + Suppress(rb) 245 | print(pat2.parseString('[Pewty]')) 246 | ``` 247 | ``` 248 | ['[', 'Pewty', ']'] 249 | ['Pewty'] 250 | ``` 251 | 252 | ## `CharsNotIn`: Match characters not in a given set 253 | 254 | ```python 255 | from pyparsing import CharsNotIn 256 | from pyparsing import nums 257 | 258 | nonDigits = CharsNotIn(nums) 259 | print(nonDigits.parseString('zoot86')) 260 | 261 | fourNonDigits = CharsNotIn(nums, exact=4) 262 | print(fourNonDigits.parseString('a$_/#')) 263 | ``` 264 | ``` 265 | ['zoot'] 266 | ['a$_/'] 267 | ``` 268 | 269 | ## `CaselessLiteral`: Case-insensitive string match 270 | 271 | ```python 272 | from pyparsing import CaselessLiteral 273 | 274 | ni = CaselessLiteral('Ni') 275 | 276 | print(ni.parseString('Ni')) 277 | print(ni.parseString('NI')) 278 | print(ni.parseString('nI')) 279 | print(ni.parseString('ni')) 280 | ``` 281 | ``` 282 | ['Ni'] 283 | ['Ni'] 284 | ['Ni'] 285 | ['Ni'] 286 | ``` 287 | 288 | ## `Forward()` 289 | 290 | Forward declaration of an expression to be defined later - used for recursive grammars, such as algebraic infix notation. When the expression is known, it is assigned to the Forward variable using the '<<' operator. 291 | 292 | ```python 293 | #!/usr/bin/env python 294 | #================================================================ 295 | # hollerith: Demonstrate Forward class 296 | #---------------------------------------------------------------- 297 | import sys 298 | from pyparsing import Word, Forward, Suppress, CaselessLiteral 299 | from pyparsing import ParseException, CharsNotIn 300 | from pyparsing import nums 301 | 302 | # - - - - - M a n i f e s t c o n s t a n t s 303 | 304 | TEST_STRINGS = [ '1HX', '2h$#', '10H0123456789', '999Hoops'] 305 | 306 | # - - - - - m a i n 307 | 308 | def main(): 309 | holler = hollerith() 310 | for text in TEST_STRINGS: 311 | test(holler, text) 312 | 313 | # - - - t e s t 314 | 315 | def test(pat, text): 316 | '''Test to see if text matches parser (pat). 317 | ''' 318 | print ("--- Test for '{0}'".format(text)) 319 | try: 320 | result = pat.parseString(text) 321 | print (" Matches: '{0}'".format(result[0])) 322 | except ParseException as x: 323 | print (" No match: '{0}'".format(str(x))) 324 | 325 | # - - - h o l l e r i t h 326 | 327 | def hollerith(): 328 | '''Returns a parser for a FORTRAN Hollerith character constant. 329 | ''' 330 | 331 | #-- 332 | # Define a recognizer for the character count. 333 | #-- 334 | intExpr = Word(nums).setParseAction(lambda t: int(t[0])) 335 | 336 | #-- 337 | # Allocate a placeholder for the rest of the parsing logic. 338 | #-- 339 | stringExpr = Forward() 340 | 341 | #-- 342 | # Define a closure that transfers the character count from 343 | # the intExpr to the stringExpr. 344 | #-- 345 | def countedParseAction(toks): 346 | '''Closure to define the content of stringExpr. 347 | ''' 348 | n = toks[0] 349 | 350 | #-- 351 | # Create a parser for any (n) characters. 352 | #-- 353 | contents = CharsNotIn('', exact=n) 354 | 355 | #-- 356 | # Store a recognizer for 'H' + contents into stringExpr. 357 | #-- 358 | stringExpr << (Suppress(CaselessLiteral('H')) + contents) 359 | 360 | return None 361 | #-- 362 | # Add the above closure as a parse action for intExpr. 363 | #-- 364 | intExpr.addParseAction(countedParseAction) 365 | 366 | #-- 367 | # Return the completed pattern. 368 | #-- 369 | return (Suppress(intExpr) + stringExpr) 370 | 371 | # - - - - - E p i l o g u e 372 | 373 | if __name__ == "__main__": 374 | main() 375 | ``` 376 | 377 | `hollerith()` 怎麼運作的,故事倒著說 378 | - `(Suppress(intExpr) + stringExpr)` 先對字串執行 `intExptr`,處理過的字串忽略結果,然後執行 `stringExpr` 379 | - `stringExptr` 是個 `Forward()` 生出來的 placeholder 380 | - `intExpr = Word(nums).setParseAction(lambda t: int(t[0]))`,匹配字串成數字,將數字解析成 `int` 381 | - `intExpr.addParseAction(countedParseAction)` 剛剛解析處來的 `int` 傳入 `countedParseAction()` 382 | - `countedParseAction()` 組合 `(Suppress(CaselessLiteral('H')) + contents)` 存到 `stringExpr` 383 | - `Suppress(CaselessLiteral('H'))` 匹配不分大小寫字母 `H`,但忽略結果 384 | - `contents = CharsNotIn('', exact=n)` 匹配n個任意字元 385 | 386 | 輸入 `10H0123456789`: 387 | - `10` 會被 `intExpr` 解釋成整數 10 (忽略結果) 388 | - `H` 會被 `Suppress(CaselessLiteral('H'))` 匹配到 (忽略結果) 389 | - `0123456789` 會被 `contents` 匹配到,取出字串 390 | 391 | ``` 392 | --- Test for '1HX' 393 | Matches: 'X' 394 | --- Test for '2h$#' 395 | Matches: '$#' 396 | --- Test for '10H0123456789' 397 | Matches: '0123456789' 398 | --- Test for '999Hoops' 399 | No match: 'Expected !W:() (at char 8), (line:1, col:9)' 400 | ``` 401 | 402 | ---- 403 | ## 參考 404 | 405 | - [Pyparsing Wiki Home](http://pyparsing.wikispaces.com/) 406 | - [pyparsing quick reference](http://infohost.nmt.edu/tcc/help/pubs/pyparsing/web/index.html) - 說明&範例 407 | - [Module pyparsing](https://pythonhosted.org/pyparsing/) 408 | -------------------------------------------------------------------------------- /executable-specifications.md: -------------------------------------------------------------------------------- 1 | # Quotes of Executable Specifications with Scrum 2 | 3 | ## Chapter 1 - Solving the Right Problem 4 | 5 | “Unfortunately, there are still too many bloated and complex software systems. Even if all team members write code correctly, more often than not, they do not efficiently solve the actual problem. There is a distinct disconnect between what the client may want or need and what is subsequently produced.” 6 | 7 | Uncertainty diagram 8 | Traditional engineering and uncertainty 9 | R&D and uncertainty 10 | Agile and uncertainty 11 | 12 | “Conversely, when almost all the risks are related to the requirements, you are in the position experienced by the majority of software development teams. This is where using an agile framework, such as Scrum, is appropriate.” 13 | 14 | ## Chapter 2 - Relying on a Stable Foundation 15 | 16 | “The most important element of this team is the presence of someone devoted full time to the specification of the software.” 17 | 18 | “The second most important element is a development team with devel- opers who have complementary skills and expertise.” 19 | 20 | “The third most important element is a product owner who ensures the development team is the ultimate product.” 21 | 22 | “The fourth and final element of importance to creating a healthy team is a team that inspects and adapts repeatedly, using prescribed events.” 23 | 24 | “Expressing a shared vision is an activity directed by the product owner. It consists mainly of face-to-face meetings with stakeholders. The result is a short, one-line summary of what the software is supposed to be and do.” 25 | 26 | “A feature is a piece of high-level functionality that delivers value to one or more stakeholders.” 27 | 28 | “The vision, the meaningful common goal, and the high-level features are guardrails because it is unlikely those will change rapidly.” 29 | 30 | ## Chapter 3 - Discovering Through Short Feedback Loops and Stakeholders’ Desirements 31 | 32 | “Deliberate discovery does not happen from the failure itself but rather from understanding the failure, making an improvement, and then trying again.” 33 | 34 | “Frequent feedback loops provide you with the ability to correct errors while costs are minimal. It is the responsibility of the team not only to learn about the problem but also to help stakeholders understand what is being built for them.” 35 | 36 | “There is a powerful and important feedback loop that occurs when stakeholders have early access to running software. They can experiment with real software, come up with new ideas, and change their minds about old ideas and perceptions.” 37 | 38 | “Fulfilling desirements through early and continuous delivery of valuable software can result in sprints that stakeholders want to evaluate.” 39 | 40 | ## Chapter 4 - Expressing Desirements with User Stories 41 | 42 | “As a ``, I want `` so that ``.” 43 | - Who = role 44 | - What = desire 45 | - Why = benefit 46 | 47 | “A well-written user story follows the INVEST mnemonic developed by Bill Wake.” 48 | - **Independent**: A story should stand alone and be self-contained without depending on other stories. 49 | - **Negotiable**: A story is a placeholder that facilitates conversation and negotiation between the team and stakeholders. At any time, the story can be rewritten or even discarded. A story is not fixed and set in stone, up until it is part of the upcoming sprint. 50 | - **Valuable**: A story needs to deliver value to the stakeholders (either the end user or the purchaser). 51 | - **Estimable**: The team needs to be able to roughly estimate the size of the effort to complete the story. 52 | - **Small**: A story can start its life as a big placeholder. As time goes by and you better understand the intricacies of the desires, the placeholder will be split into smaller stories. When the most important ones are close to being delivered, they need to be small enough so that they can be completed in a single sprint. 53 | - **Testable**: A story must provide the necessary information to clearly define the acceptance criteria that confirm the story is completed. 54 | 55 | ## Chapter 5 - Refining User Stories by Grooming the Product Backlog 56 | 57 | “The product owner is responsible for ensuring that the product backlog is always in a healthy state. He is the primary interface between the development team and the stakeholders.” 58 | 59 | “There is a major difference between a true analyst and a product owner. Product owners represent the business and have the authority to make decisions that affect their product. Typically, an analyst does not have this decision-making authority.” 60 | 61 | “Grooming the backlog boils down to a sequence of four activities: ranking, illustrating, sizing, and splitting user stories.” 62 | - Ranking User Stories with a Dot Voting Method 63 | - Illustrating User Stories with Storyboards 64 | - Sizing User Stories Using Comparison 65 | - Splitting User Stories Along Business Values 66 | 67 | “Although, according to the development team, the product owner is perceived as the one who decides the ordering of the backlog, it is actually not his decision. He must rely on stakeholders who are the ones who decide the importance of each story.” 68 | 69 | “The product owner is a facilitator, not a decider.” 70 | 71 | “If user stories help monitor conversations with stakeholders, storyboards help to illustrate expectations rapidly and cheaply.” 72 | 73 | “As experience teaches, stakeholders love to envision the software from the user interface standpoint.” 74 | 75 | “Only the development team can identify the size of a story.” 76 | 77 | “Humans are poor at estimating absolute sizes. However, we are great at assessing relative sizes.” 78 | 79 | “The product owner should not plan stories that are bigger than one-half the velocity.” 80 | 81 | “You should focus on the perspective of stakeholders by thin slicing stories that favor the business value. Thin slicing is based on evolutionary architecture; it provides stories that implement only a small bit of functionality, but all the way through the architecture layers of the software.” 82 | 83 | “In this regard, over the years, experienced practitioners have acknowledged the necessity of structuring the backlog along a two-dimensional collaboration board. This way of organizing the stories to avoid half-baked incremental iterations was initially promoted by Jeff Patton and is now known as story mapping.” 84 | 85 | “When a story has gone through the process of grooming, you have reached an important milestone, which is the transition from conversation to confirmation.” 86 | 87 | ## Chapter 6 - Confirming User Stories with Scenarios 88 | 89 | “If user stories and their storyboards help monitor conversations with stakeholders during backlog grooming, the scenarios help to confirm expectations when the team is ready to plan a new sprint.” 90 | 91 | “Success criteria establish the conditions of acceptation from the stakeholders’ point of view. Scenarios are the perfect medium for expressing the success criteria.” 92 | 93 | “The scenario contains a precondition, an action, and a consequence.” 94 | - `Given` A precondition is the current state of the software before action is taken. 95 | - `When` An action is something that is accomplished to perform the behavior of the scenario. 96 | - `Then` A consequence is the result of the action. 97 | 98 | “Triggering a single action is crucial in keeping the state transition simple.” 99 | 100 | “Successful teams don’t use raw examples, they refine the specification from them. They extract the essence from the key examples and turn them into a clear and unambiguous definition of what makes the implementation done, without any extraneous detail.” 101 | 102 | “It is important to note that the analyst’s competency is related to the specifications and not to how the software will be implemented.” 103 | 104 | “The role of business analysts is now more focused and can be summarized as ensuring that all the scenarios illustrating a story are refined and completed in time for the development team.” 105 | 106 | “Because designing the technical solution is not the purpose of the specification, you should focus only on writing scenarios that relate to the business rules.” 107 | 108 | “‘Why?’ five times, successively, helps the team understand the true root cause of the scenario and easily reformulate it at the business domain level.” 109 | 110 | “A feature is a piece of high-level functionality that delivers value to one or more stakeholders.” 111 | 112 | ## Chapter 7 - Automating Confirmation with Acceptance Tests 113 | 114 | “You must turn scenarios into acceptance tests with minimal changes. An acceptance test is only a copy of a scenario in a format suitable for execution on a computer.” 115 | 116 | “First and foremost, we want to confirm requirements have been met. These “executable” scenarios are not a quality assurance tool. They are used to prevent defects, not to discover them.” 117 | 118 | “these “executable” scenarios do not replace the need to include quality assurance practices, such as exploratory or unit testing.” 119 | 120 | “It goes without saying that the widespread approach of creating tests using record-and-playback tools is inappropriate. ... Their main advantage, which is to enable testers to author tests without having to learn how to craft code, is also their main weakness.” 121 | 122 | “Acceptance tests represent assumptions stakeholders made during the specification.” 123 | 124 | “The red-green-refactor cycle is the core of Test-Driven Development (TDD). It is a widely recognized programming practice popularized by Kent Beck that promotes the notion of writing tests first when programming a piece of code. TDD relies on the repetition of a short development cycle divided into three stages: the red, the green, and the refactor stage.” 125 | 126 | “TDD requires programmers to articulate their assumptions using a test case. Programmers must foresee how the functionality will be used by the test case. TDD places constraints on programmers; it requires that they define the interface before deciding on the implementation. TDD tends to lead to better designs.” 127 | 128 | “If the development team members are the only ones who understand the result of the translation, they lose the ability to collaborate effectively with stakeholders.” 129 | 130 | “To obtain a failing assertion, the tester must design the programming interface and connect the newly created test with it.” 131 | 132 | “When connecting the newly created test, the tester focuses first on defining the outside-facing programming interface and only after that does the programmer go on evolving the internal implementation.” 133 | 134 | “The natural candidates for this type of design are the testers because they are the ones responsible for creating a failing acceptance test.” 135 | 136 | “You must resist this temptation to mix acceptance tests with continuous integration. Because of hardware constraints, this can unduly slow down the code integration.” 137 | 138 | “Testing the “executable” scenarios during the nightly build ensures that every morning the team can easily confirm that the software under construction still meets the evolving specifications.” 139 | 140 | ## Chapter 8 - Addressing Nonfunctional Requirements 141 | 142 | “External quality is how well the software carries out its functions at run time, and as such, is not only visible to stakeholders, but is also highly desirable.” 143 | 144 | “Internal quality is characteristics of the software barely visible to stakeholders but which simplifies the process of building and evolving the software.” 145 | 146 | ## Chapter 9 - Conclusion 147 | 148 | “If you are going to put time and effort into solving a problem, ensure that you first solve the right problem and then that you solve it properly.” 149 | 150 | “To be successful, you must remember that above all else, needs are emergent and constantly evolving. There is no set plan that can and will be successful. Instead, it is a question of constantly being open to changes and uncertainties. It is only when flexibility is embraced that the proper attitude can be taken.” 151 | 152 | ---- 153 | ## 參考 154 | - [Executable Specifications with Scrum](http://www.ibchamber.org/wp-content/uploads/2014/09/AWP.Executable.Specifications.with_.Scrum_.Jul_.2013.pdf) 155 | -------------------------------------------------------------------------------- /unittest.mock.md: -------------------------------------------------------------------------------- 1 | # unittest.mock 2 | 3 | unittest.mock 是 python 用於測試的函式庫,用 mock 物件替換待測試系統的某些部分,宣告這些偽造的部分應該如何被使用。 4 | 5 | ## Test Double 6 | 7 | [介紹Test Double](http://teddy-chen-tw.blogspot.tw/2014/09/test-double1.html)之前先介紹兩個測試常用術語: 8 | 9 | - SUT:System Under Test或Software Under Test的簡寫,代表待測程式。如果是單元測試,SUT就是一個function或method。 10 | - DOC:Depended-on Component(相依元件),又稱為Collaborator(合作者)。DOC是SUT執行的時候會使用到的元件。例如,有一個函數X如果執行失敗會寄送email,則email元件就是函數X的DOC。 11 | 12 | [Test Double 的種類](http://www.martinfowler.com/bliki/TestDouble.html): 13 | - **Dummy** 用來傳遞給函式但不會真的被使用,通常只充當函式參數讓程式順利編譯。 14 | - **Fake** 物件真正功能,但通常會採用捷徑實作,不適合用在生產環境。(範例:InMemoryTestDatabase) 15 | - **Stub** 為測試時期的呼叫提供罐裝答案,通常不會對外界所有輸入做出反應。 16 | - **Spy** 記錄他們怎麼被呼叫的資訊。 17 | - **Mock** 是一連串預先編排的執行動作,對特定預期的呼叫做出反應,如果收到非預期呼叫方式則丟出例外。 18 | 19 | 20 | 21 | ## 快速導覽 22 | 23 | ### 範例一 24 | Something.py: 25 | ```python 26 | class Something: 27 | def method(self, a, b, c, key): 28 | print(a, b, c, key) 29 | return 3 30 | ``` 31 | 32 | testSomething.py 33 | ```python 34 | import unittest 35 | from unittest.mock import MagicMock 36 | from Something import Something 37 | 38 | class TestSomethingCases(unittest.TestCase): 39 | 40 | def test_return_value(self): 41 | s = Something() 42 | self.assertEqual(s.method(1,2,3,key='hello'), 3) 43 | 44 | def test_mock_return_value(self): 45 | something = Something() 46 | something.method = MagicMock(return_value = 5) 47 | self.assertEqual(something.method(1, 2, 3, key='hello'), 5) 48 | 49 | def test_mock_called_with(self): 50 | something = Something() 51 | something.method = MagicMock(return_value = 5) 52 | something.method(3,4,5,key='value') 53 | something.method.assert_called_with(3, 4, 5, key='value') 54 | ``` 55 | - `test_return_value` 檢查物件方法預設回傳值 56 | - `test_mock_return_value` 偽造方法回傳值 (Stub) 57 | - `test_mock_called_with` 檢查方法呼叫是否如預期 (Spy or Mock) 58 | 59 | testing: 60 | ```shell 61 | $ python -m unittest -v testSomething 62 | test_mock_called_with (testSomething.TestSomethingCases) ... ok 63 | test_mock_return_value (testSomething.TestSomethingCases) ... ok 64 | test_return_value (testSomething.TestSomethingCases) ... 1 2 3 hello 65 | ok 66 | 67 | ---------------------------------------------------------------------- 68 | Ran 3 tests in 0.004s 69 | 70 | OK 71 | ``` 72 | 73 | ### 範例二 74 | 75 | `side_effect` 允許執行副作用,包含當 mock 被呼叫時產生例外。 76 | 77 | ```python 78 | >>> from unittest.mock import Mock 79 | >>> mock = Mock(side_effect=KeyError('foo')) 80 | >>> mock() 81 | Traceback (most recent call last): 82 | File "", line 1, in 83 | File "/usr/lib/python3.4/unittest/mock.py", line 902, in __call__ 84 | return _mock_self._mock_call(*args, **kwargs) 85 | File "/usr/lib/python3.4/unittest/mock.py", line 958, in _mock_call 86 | raise effect 87 | KeyError: 'foo' 88 | ``` 89 | - 呼叫 `mock` 產生例外 90 | 91 | ```python 92 | >>> values ={'a' : 1, 'b' : 2, 'c' : 3} 93 | >>> def side_effect(arg): 94 | ... return values[arg] 95 | ... 96 | >>> mock.side_effect = side_effect 97 | >>> mock('a'), mock('b'), mock('c') 98 | (1, 2, 3) 99 | ``` 100 | - 透過 `side_effect()` 控制呼叫 `mock` 時回應的值 101 | 102 | ```python 103 | >>> mock.side_effect = [5, 4, 3, 2, 1] 104 | >>> mock(), mock(), mock() 105 | (5, 4, 3) 106 | ``` 107 | - 預設呼叫 `mock` 回應的值 108 | 109 | ### 範例三 110 | 111 | ---- 112 | 113 | ## Using Mock 114 | 115 | ### Mock Patching Methods 116 | 117 | Mock 通常用在 118 | 119 | - 取代物件方法 (stub) 120 | - 檢查物件方法呼叫是否合乎預期 (spy) 121 | 122 | ```python 123 | >>> from unittest.mock import MagicMock 124 | >>> class SomeClass: 125 | ... def method(self, a, b, c): 126 | ... return a + b + c 127 | ... 128 | >>> real = SomeClass() 129 | >>> real.method(1,2,3) 130 | 6 131 | >>> real.method = MagicMock(name='method') 132 | >>> real.method(2,3,4) 133 | 134 | >>> real.method.assert_called_with(2,3,4) 135 | ``` 136 | ### Mock for Method Calls on an Object 137 | 138 | 傳遞物件給方法,檢查物件是否被正確使用 139 | 140 | ```python 141 | >>> class ProductionClass: 142 | ... def closer(self, something): 143 | ... something.close() 144 | ... 145 | >>> real = ProductionClass() 146 | >>> mock = MagicMock() 147 | >>> real.closer(mock) 148 | >>> mock.close.assert_called_with() 149 | ``` 150 | - `ProductionClass.closer` 會呼叫傳入參數的方法 `something.close()` 151 | - `mock = MagicMock()` 產生 spy 傳入方法中,然後檢是否被正確呼叫 152 | 153 | ### Mocking Classes 154 | 155 | 常見的情況是要在測試中用替換某類別,當你 patch 一個類別,這個類別就被 mock 取代。類別的物件是在被呼叫的方法中產生,你可以藉由查看被偽裝類別的回傳值存取偽裝物件。 156 | 157 | > 不用 IoC 嗎? 158 | 159 | module.py: 160 | ```python 161 | class Foo: 162 | def method(self): 163 | return 'foo' 164 | ``` 165 | 166 | software under test: 167 | ```python 168 | >>> import module 169 | >>> def some_function(): 170 | ... instance = module.Foo() 171 | ... return instance.method() 172 | ... 173 | >>> some_function() 174 | 'foo' 175 | ``` 176 | 177 | 用 mock 取代 `module.Foo` 178 | ```python 179 | >>> from unittest.mock import patch 180 | >>> with patch('module.Foo') as mock: 181 | ... instance = mock.return_value 182 | ... instance.method.return_value = 'bar 183 | ... result = some_function() 184 | ... assert result == 'bar' 185 | ``` 186 | 187 | ### Naming your mocks 188 | 189 | 命名 mock 物件,當出現錯誤可以快速找到原因 190 | 191 | ```python 192 | >>> from unittest.mock import MagicMock 193 | >>> mock = MagicMock(name='foo') 194 | >>> mock 195 | 196 | >>> mock.method 197 | 198 | ``` 199 | 200 | ### Tracking all Calls 201 | 202 | `mock_calls` 屬性紀錄所有呼叫 mock 與其子類的歷程 203 | 204 | ```python 205 | >>> from unittest.mock import MagicMock 206 | >>> mock = MagicMock() 207 | >>> mock.method(1) 208 | 209 | >>> mock.method(2) 210 | 211 | >>> mock.method(3) 212 | 213 | >>> mock.mock_calls 214 | [call.method(1), call.method(2), call.method(3)] 215 | ``` 216 | ### Setting Return Values and Attributes 217 | 218 | 設定 mock 回傳值很簡單 219 | ```python 220 | >>> from unittest.mock import Mock 221 | >>> mock = Mock() 222 | >>> mock.return_value = 3 223 | >>> mock() 224 | ``` 225 | 226 | 可以定義 mock 方法的回傳值 227 | ```python 228 | >>> mock = Mock() 229 | >>> mock.method.return_value = 3 230 | >>> mock.method() 231 | 3 232 | ``` 233 | 234 | 可以在建構函式中宣告回傳值 235 | ```python 236 | >>> mock = Mock(return_value = 3) 237 | >>> mock() 238 | 3 239 | ``` 240 | 241 | 可以設定 mock 的屬性值 242 | ```python 243 | >>> mock = Mock() 244 | >>> mock.x = 3 245 | >>> mock.x 246 | 3 247 | ``` 248 | 249 | 有時候要設定更複雜的情況,例如 `mock.connection.cursor().execute("SELECT 1")`,如果希望回傳一個陣列,就要設定一個遞迴呼叫的回傳結果。 250 | ```python 251 | >>> from unittest.mock import call 252 | >>> mock = Mock() 253 | >>> cursor = mock.connection.coursor.return_value 254 | >>> cursor.execute.return_value = ['foo'] 255 | >>> mock.connection.coursor().execute("SELECT 1") 256 | ['foo'] 257 | >>> expected = call.connection.coursor().execute("SELECT 1").call_list() 258 | >>> mock.mock_calls 259 | [call.connection.coursor(), call.connection.coursor().execute('SELECT 1')] 260 | >>> mock.mock_calls == expected 261 | True 262 | ``` 263 | - `cursor = mock.connection.coursor.return_value` 產生第一個回傳值,回傳一個 `cursor` 264 | - `cursor.execute.return_value = ['foo']` 設定執行第一個回傳值的結果,回傳一個陣列 265 | 266 | 使用偽造的 cursor 查詢資料庫 267 | ```python 268 | >>> cursor.execute("SELECT 1") 269 | ['foo'] 270 | ``` 271 | 272 | ### Raising exceptions with mocks 273 | 274 | 做出一個會產生例外的 mock 275 | ```python 276 | >>> from unittest.mock import Mock 277 | >>> mock = Mock(side_effect=Exception('Boom!')) 278 | >>> mock() 279 | Traceback (most recent call last): 280 | File "", line 1, in 281 | File "/usr/lib/python3.4/unittest/mock.py", line 902, in __call__ 282 | return _mock_self._mock_call(*args, **kwargs) 283 | File "/usr/lib/python3.4/unittest/mock.py", line 958, in _mock_call 284 | raise effect 285 | Exception: Boom! 286 | ``` 287 | 288 | ### Side effect functions and iterables 289 | 290 | `side_effect` 可以設定為函數或 **iterable**。當 mock 預計要被多次呼叫,每次回傳不同的值。當設定 side_effect 成為 iterable,每次呼叫會從 iterable 回傳 next value。 291 | 292 | ```python 293 | >>> from unittest.mock import MagicMock 294 | >>> mock = MagicMock(side_effect=[4,5,6]) 295 | >>> mock() 296 | 4 297 | >>> mock() 298 | 5 299 | >>> mock() 300 | 6 301 | >>> mock() 302 | Traceback (most recent call last): 303 | File "", line 1, in 304 | File "/usr/lib/python3.4/unittest/mock.py", line 902, in __call__ 305 | return _mock_self._mock_call(*args, **kwargs) 306 | File "/usr/lib/python3.4/unittest/mock.py", line 961, in _mock_call 307 | result = next(effect) 308 | StopIteration 309 | ``` 310 | 311 | 更進階用法是根據傳入參數變化回傳的值,此時 side_effect 可以是個 **function**。下例透過 dictionary 偽造傳入參數與回傳值的對應關係。 312 | ```python 313 | >>> vals = {(1,2): 1, (2,3): 2} 314 | >>> def side_effect(*args): 315 | ... return vals[args] 316 | ... 317 | >>> mock = MagicMock(side_effect=side_effect) 318 | >>> mock(1,2) 319 | 1 320 | >>> mock(2,3) 321 | 2 322 | ``` 323 | 324 | ### Creating a Mock from an Existing Object 325 | 326 | 有時隨著時間演進,測試與待測物變得不匹配,例如 `Foo` 原先有 `old_method` 方法,但後來改成 `method`。使用 spec 關鍵字,當存取的方法或屬性不存在於 spec 定義的物件中,就會立即產生錯誤。 327 | 328 | ```python 329 | >>> class Foo: 330 | ... def method(): 331 | ... return 'foo' 332 | ... 333 | >>> mock = Mock(spec=Foo) 334 | >>> mock.method.return_value = 'bar' 335 | >>> mock.method() 336 | 'bar' 337 | >>> mock.old_method.return_value = 'baz' 338 | Traceback (most recent call last): 339 | File "", line 1, in 340 | File "/usr/lib/python3.4/unittest/mock.py", line 574, in __getattr__ 341 | raise AttributeError("Mock object has no attribute %r" % name) 342 | AttributeError: Mock object has no attribute 'old_method' 343 | ``` 344 | 345 | 也可以使用 spec 定義呼叫 mock 的方法,不管參數是根據位置傳遞或是依照參數名稱傳遞。 346 | ```python 347 | >>> def f(a,b,c): pass 348 | ... 349 | >>> mock = Mock(spec=f) 350 | >>> mock(1,2,3) 351 | 352 | >>> mock.assert_called_with(a=1, b=2, c=3) 353 | >>> mock(c=3, b=2, a=1) 354 | 355 | >>> mock.assert_called_with(a=1, b=2, c=3) 356 | ``` 357 | 358 | ## Patch Decorators 359 | 360 | “使用patch或者patch.object的目的是为了控制mock的范围,意思就是在一个函数范围内,或者一个类的范围内,或者with语句的范围内mock掉一个对象。” - [Python Mock的入门](https://segmentfault.com/a/1190000002965620) 361 | 362 | - [`patch()`](https://docs.python.org/3.4/library/unittest.mock.html#unittest.mock.patch) acts as a function decorator, class decorator or a context manager. Inside the body of the function or with statement, the target is patched with a new object. When the function/with statement exits the patch is undone. 363 | - [`patch.object()`](https://docs.python.org/3.4/library/unittest.mock.html#unittest.mock.patch.object) can be used as a decorator, class decorator or a context manager. Arguments new, spec, create, spec_set, autospec and new_callable have the same meaning as for patch(). Like patch(), patch.object() takes arbitrary keyword arguments for configuring the mock object it creates. 364 | 365 | Game.py: 366 | ```python 367 | import random 368 | 369 | def choice(*seq): 370 | return random.choice(seq) 371 | 372 | class Game: 373 | def coin(self): 374 | return choice(['head', 'tail']) 375 | 376 | def bet(self, side): 377 | return 'win' if side == self.coin() else 'lose' 378 | ``` 379 | - `choice()` 隨機回傳 seq 裡面任意元素 380 | - `coin()` 透過 `choice()` 決定硬幣正反面 381 | - `bet()` 判斷 `side` 與 `coin()` 是否相同,決定輸贏 382 | 383 | testGame.py: 384 | ```python 385 | import unittest 386 | from unittest.mock import patch 387 | 388 | class TestGame(unittest.TestCase): 389 | 390 | def test_head_tail(self): 391 | def always_tail(self): 392 | return 'tail' 393 | with patch('Game.choice', always_tail): 394 | from Game import Game 395 | game = Game() 396 | self.assertEqual(game.bet('head'), 'lose') 397 | 398 | def test_head_head(self): 399 | from Game import Game 400 | def always_head(self): 401 | return 'head' 402 | with patch.object(Game, 'coin', always_head): 403 | game = Game() 404 | self.assertEqual(game.bet('head'), 'win') 405 | ``` 406 | 407 | 為了測試 `Game.bet` 裡面判斷輸贏的邏輯是否正確,必須將底層的依賴元件取代為可操作結果的 test double。取代有兩種方式,`patch` 與 `patch.object`: 408 | 409 | - `test_head_tail`: 用 `always_tail` 取代 `Game` 模組的 `choice` 函式 410 | - `test_head_head`: 用 `always_head` 取代 `Game` 類別的 `coni` 方法 411 | 412 | run test: 413 | ```chell 414 | $ python -m unittest -v test 415 | test_head_head (test.TestGame) ... ok 416 | test_head_tail (test.TestGame) ... ok 417 | 418 | ---------------------------------------------------------------------- 419 | Ran 2 tests in 0.010s 420 | 421 | OK 422 | ``` 423 | 424 | ## Further Examples 425 | 426 | ### Mocking chained calls 427 | 428 | 一旦理解 `return_value` 這個屬性,偽裝一連串的呼叫使用上會很直覺。當 `mock` 第一次被呼叫,或是在呼叫前讀取 `return_value`,新的 `mock` 物件就因應而生。 429 | 430 | ```python 431 | >>> from unittest.mock import Mock 432 | >>> mock = Mock() 433 | >>> mock().foo(a=2,b=3) 434 | 435 | >>> mock.return_value.foo.assert_called_with(a=2,b=3) 436 | ``` 437 | - 呼叫 `mock()` 則產生一個新的 mock 物件,然後繼續呼叫新 mock 物件的方法 `foo(a=2,b=3)` 438 | - 最後用 `mock.return_value.foo.assert_called_with(a=2,b=3)` 檢查一開始的 `mock` 產生的物件 `mock.return_value` 的 `foo` 方法是否以 `(a=2,b=3)` 方式呼叫 439 | 440 | ### Partial mocking 441 | ### Mocking a Generator Method 442 | ### Applying the same patch to every test method 443 | ### Mocking Unbound Methods 444 | ### Checking multiple calls with mock 445 | ### Coping with mutable arguments 446 | ### Nesting Patches 447 | ### Mocking a dictionary with MagicMock 448 | ### Mock subclasses and their attributes 449 | ### Mocking imports with patch.dict 450 | ### Tracking order of calls and less verbose call assertions 451 | ### More complex argument matching 452 | 453 | ---- 454 | ## 參考 455 | - [unittest.mock — mock object library 456 | ](https://docs.python.org/3.4/library/unittest.mock.html) 457 | - [unittest.mock — getting started](https://docs.python.org/3.4/library/unittest.mock-examples.html) 458 | -------------------------------------------------------------------------------- /jenkins.md: -------------------------------------------------------------------------------- 1 | # Jenkins 2 | 3 | Jenkins is an award-winning, cross-platform, **continuous integration** and **continuous delivery** application that increases your productivity. Use Jenkins to build and test your software projects continuously making it easier for developers to integrate changes to the project, and making it easier for users to obtain a fresh build. It also allows you to continuously deliver your software by providing powerful ways to define your build pipelines and integrating with a large number of testing and deployment technologies. - 摘錄 [Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Meet+Jenkins) 官網介紹 4 | 5 | ## 安裝 6 | 7 | 參考: https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu 8 | 9 | 依照官網說明安裝 Jenkins 非常簡單 10 | ```shell 11 | $ wget -q -O - https://jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add - 12 | $ sudo sh -c 'echo deb http://pkg.jenkins-ci.org/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' 13 | $ sudo apt-get update 14 | $ sudo apt-get install jenkins 15 | ``` 16 | 17 | 安裝完後,Jenkins 預設已經啟動 18 | ```shell 19 | $ ps aux | grep jenkins 20 | jenkins 10670 0.0 0.0 18596 172 ? S 16:27 0:00 /usr/bin/daemon --name=jenkins --inherit --env=JENKINS_HOME=/var/lib/jenkins --output=/var/log/jenkins/jenkins.log --pidfile=/var/run/jenkins/jenkins.pid -- /usr/bin/java -Djava.awt.headless=true -jar /usr/share/jenkins/jenkins.war --webroot=/var/cache/jenkins/war --httpPort=8080 --ajp13Port=-1 21 | jenkins 10671 9.9 35.4 1186188 179532 ? Sl 16:27 0:34 /usr/bin/java -Djava.awt.headless=true -jar /usr/share/jenkins/jenkins.war --webroot=/var/cache/jenkins/war --httpPort=8080 --ajp13Port=-1 22 | vagrant 10752 0.0 0.4 12720 2124 pts/1 S+ 16:33 0:00 grep --color=auto jenkins 23 | ``` 24 | 25 | 手動設定 Jenkins 服務 26 | ```shell 27 | $ sudo service jenkins start # 啟動 28 | $ sudo service jenkins stop # 停止 29 | $ sudo service jenkins restart # 重新啟動 30 | ``` 31 | 32 | ## 啟動與存取 33 | 34 | 參考: https://wiki.jenkins-ci.org/display/JENKINS/Starting+and+Accessing+Jenkins 35 | 36 | 啟動 Jenkins 最簡單的方式 37 | ```shell 38 | $ java -jar jenkins.war 39 | ``` 40 | 41 | 打開瀏覽器,開啟 http://192.168.33.10:8080/ (192.168.33.10 是[虛擬機](environment.md)的IP),就能看到管理介面 42 | 43 | ### 變更語言 44 | 45 | 啟動 Jenkins 後,因為瀏覽器語系關係為顯示為中文操作介面,想改成與官網說明一樣的英文介面,依照下面步驟 46 | 47 | - 安裝插件:「管理 Jenkins」→「管理外掛程式」→「過濾條件」輸入 `locale` →「下載並於重新啟動後安裝」 48 | - 變更語系:「管理 Jenkins」→「設定系統」→「預設語言」輸入 `en_US` →「Ignore browser preference and force this language to all users」 49 | 50 | ![Jenkins](jenkins.png) 51 | 52 | ### Jenkins 功能階層圖 53 | ``` 54 | Jenkins Home 55 | |-- Jenkins configure 56 | |-- Build Jobs 57 | |-- Job_A 58 | | |-- Job Configure 59 | | |-- Build History 60 | | |-- Build #1 61 | | |-- Build #2 62 | | |-- Build #3 63 | |-- Job_B 64 | |-- Job_C 65 | ``` 66 | 67 | ## 實驗一:“Hello World” 68 | 69 | ### 練習目標 70 | 71 | - 在開發環境上 72 | - 無 73 | 74 | - 在 Jenkins Server 上 75 | - 建立簡單的 Build Job 76 | - 手動執行 Build Job 77 | - 自動執行 Build Job (週期性) 78 | 79 | ### 在 Jenkins Server 上 80 | 81 | #### 建立 Build Job 82 | 83 | - 到 Jenkins 首頁,點選「New Item」 84 | - 「Item name」填入 `myBuild`,選擇「Freestyle project」,接著進入設定 Build Job 細節頁面 85 | - 「Build」內按下「Add build step」,選擇「Execute shell」,「Command」填入下面 shell script 86 | - 按下「Save」儲存離開 87 | 88 | ```shell 89 | #!/bin/bash 90 | echo "Hello World" 91 | ``` 92 | 93 | #### 手動執行 94 | 95 | - 到「myBuild」頁面,點選「Build Now」 96 | - 看到「Build History」出現 Build item,點選 #1 97 | - 點選「Console Output」,看到以下 Build process 98 | 99 | ``` 100 | Started by user anonymous 101 | Building in workspace /var/lib/jenkins/jobs/myProject/workspace 102 | [workspace] $ /bin/bash /tmp/hudson6372466822262253605.sh 103 | Hello World 104 | Finished: SUCCESS 105 | ``` 106 | 107 | #### 自動執行 108 | 109 | - 到「myBuild」頁面,點選「Configure」 110 | - 「Build Triggers」下點選「Build periodically」,「Schedule」填入 `* * * * *` (表示每分鐘 build 一次) 111 | - 按下「Save」儲存離開 112 | - 等待數分鐘,看到「Build History」出現多個 Build item 113 | 114 | ## 實驗二:配合 SCM 進行自動化建置 115 | 116 | Jenkins 能根據 Source Code Management (SCM) 上程式碼的變化觸發建置與測試,除了預設的 CVS 還有許多 plugin 支援各式各樣的版本控制系統,如 Accurev, Bazaar, BitKeeper, ClearCase, CMVC, Dimensions, Git, CA Harvest, Mercurial, Perforce, PVCS, StarTeam, CM/Synergy, Microsoft Team Foundation Server, and even Visual SourceSafe。 (資料來源[Jenkins: The Definitive Guide](https://www.safaribooksonline.com/library/view/jenkins-the-definitive/9781449311155/ch05s04.html)) 117 | 118 | ### 練習目標 119 | 120 | - 在開發環境上 121 | - 建立專案,設定 Git repository 122 | 123 | - 在 Jenkins Server 上 124 | - 安裝 plugin (Git) 125 | - 隨著 Git repository 更新,進行自動化建置 126 | 127 | ### 在開發環境上 128 | 129 | #### 建立工作目錄 130 | 131 | ```shell 132 | $ mkdir myWorkspace 133 | $ cd myWorkspace 134 | ``` 135 | 136 | #### 設定 Python 版本 137 | 138 | ```shell 139 | $ pyenv local 3.5.1 140 | $ python --version 141 | Python 3.5.1 142 | ``` 143 | 144 | #### 建立虛擬環境 145 | 146 | ```shell 147 | $ pyvenv venv 148 | ``` 149 | 150 | #### 切換虛擬環境 151 | 152 | ```shell 153 | $ pyvenv venv 154 | $ source venv/bin/activate 155 | (venv) $ pip install --upgrade pip 156 | ``` 157 | 158 | > 出現 (venv) 提示表示目前使用 Python virtualenv,往後範例省略顯示 159 | 160 | #### 建立專案目錄 161 | 162 | ```shell 163 | $ mkdir myProject 164 | $ cd myProject 165 | $ pwd 166 | /home/vagrant/myWorkspace/myProject 167 | ``` 168 | 169 | #### 產生 HelloWorld.py 170 | 171 | ```shell 172 | $ echo 'print("Hello World")' > HelloWorld.py 173 | $ python HelloWorld.py 174 | Hello World 175 | ``` 176 | 177 | #### 初始化 Git repository 178 | 179 | ```shell 180 | $ git init 181 | ``` 182 | 183 | #### 將 HelloWorld.py 加入 Git repository 184 | 185 | ```shell 186 | $ git add . 187 | $ git commit -m "add a python file" 188 | ``` 189 | 190 | ### 在 Jenkins Server 上 191 | 192 | #### 安裝 Git plugin 193 | 194 | - 到 Jenkins 首頁,選擇「Manage Jenkins」 195 | - 點選「Manage Plugins」,進入設定插件管理頁面 196 | - 選擇「Available」標籤,「filter」輸入 `Git plugin` 197 | - 選取「Git plugin」,按下「Install without restart」 198 | - 等候安裝完成 199 | 200 | #### 修改 Build Job 201 | 202 | - 到「myBuild」頁面,點選「Configure」 203 | - 「Source Code Management」下選擇「Git」,「Repository URL」填入 `file:///home/vagrant/myWorkspace/myProject` 204 | - 「Build Triggers」,取消「Build periodically」,改選取「Poll SCM」,「Schedule」填入 `* * * * *` (表示每分鐘查詢 git repository 一次,如果 git repository 有更新則觸發 Build Job) 205 | - 「Build」下「Execute shell」,「Command」改成下面 shell script 206 | - 按下「Save」儲存離開 207 | 208 | ```shell 209 | #!/bin/bash 210 | python --version 211 | python HelloWorld.py 212 | ``` 213 | 214 | 如果 git repository 在 Jenkins Server 上從未建置過、或有任何更新產生,會在一分鐘內看到自動執行的 Build result 215 | 216 | ```shell 217 | Started by an SCM change 218 | Building in workspace /var/lib/jenkins/jobs/myProject/workspace 219 | Cloning the remote Git repository 220 | Cloning repository file:///home/vagrant/myWorkspace/myProject 221 | > git init /var/lib/jenkins/jobs/myProject/workspace # timeout=10 222 | Fetching upstream changes from file:///home/vagrant/myWorkspace/myProject 223 | > git --version # timeout=10 224 | > git -c core.askpass=true fetch --tags --progress file:///home/vagrant/myWorkspace/myProject +refs/heads/*:refs/remotes/origin/* 225 | > git config remote.origin.url file:///home/vagrant/myWorkspace/myProject # timeout=10 226 | > git config --add remote.origin.fetch +refs/heads/*:refs/remotes/origin/* # timeout=10 227 | > git config remote.origin.url file:///home/vagrant/myWorkspace/myProject # timeout=10 228 | Fetching upstream changes from file:///home/vagrant/myWorkspace/myProject 229 | > git -c core.askpass=true fetch --tags --progress file:///home/vagrant/myWorkspace/myProject +refs/heads/*:refs/remotes/origin/* 230 | > git rev-parse refs/remotes/origin/master^{commit} # timeout=10 231 | > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10 232 | Checking out Revision 8e045c06247802b5d08f757c0a9fe467a3700424 (refs/remotes/origin/master) 233 | > git config core.sparsecheckout # timeout=10 234 | > git checkout -f 8e045c06247802b5d08f757c0a9fe467a3700424 235 | First time build. Skipping changelog. 236 | [workspace] $ /bin/bash /tmp/hudson105525603757121577.sh 237 | Python 2.7.9 238 | Hello World 239 | Finished: SUCCESS 240 | ``` 241 | 242 | 雖然建置成功,但是 `python --version` 顯示 Python 2.7.9,跟開發環境使用的版本 Python 3.5.1 不同。接下來要讓 Jenkins Server 的執行環境跟開發環境保持一致。 243 | 244 | ## 實驗三:設定環境 245 | 246 | ### 練習目標 247 | 248 | - 在開發環境上 249 | - 無 250 | 251 | - 在 Jenkins Server 上 252 | - 安裝 plugin (pyenv) 253 | - 設定 Build environment 254 | 255 | ### 在 Jenkins Server 上 256 | 257 | #### 安裝 pyenv plugin 258 | 259 | - 到 Jenkins 首頁,選擇「Manage Jenkins」 260 | - 點選「Manage Plugins」,進入設定插件管理頁面 261 | - 選擇「Available」標籤,「filter」輸入 `pyenv plugin` 262 | - 選取「Git plugin」,按下「Install without restart」 263 | - 等候安裝完成 264 | 265 | > 我的經驗:安裝 pvenv plugin 後要重新啟動 jenkins,這個 plugin 才會生效 266 | 267 | #### 修改 Build Job 268 | 269 | - 到「myBuild」頁面,點選「Configure」 270 | - 「Build Environment」內選取「pyenv build wrapper」,「The Python version」填寫 `3.5.1` 271 | - 按下「Save」儲存離開 272 | 273 | 修改 myBuild Configure,不會觸發 Build Job,手動執行後觀察結果 274 | 275 | - 到「myBuild」頁面,點選「Build Now」 276 | - 看到「Build History」出現 Build item,點選最新的 Build result 277 | - 點選「Console Output」,看到以下 Build process 278 | 279 | ``` 280 | ... (略) 281 | Python 3.5.1 282 | Hello World 283 | Finished: SUCCESS 284 | ``` 285 | 286 | 除了執行程式,Jenkins 還可以將自動執行單元測試。接下來看看 Jenkins 怎麼將瑣碎的手動步驟變成自動化。 287 | 288 | ## 實驗四:自動化單元測試 289 | 290 | ### 練習目標 291 | 292 | - 在開發環境上 293 | - 建立一個 Arithmetic 類別與單元測試 294 | - 手動執行測試 295 | 296 | - 在 Jenkins Server 上 297 | - 設定自動化測試 298 | 299 | ### 在開發環境上 300 | 301 | #### 新增檔案 Arithmetic.py 302 | 303 | ```python 304 | def add(a, b): 305 | return a + b 306 | 307 | def subtract(a, b): 308 | return a - b 309 | 310 | def multiply(a, b): 311 | return a * b 312 | 313 | def divide(a, b): 314 | return a / b 315 | 316 | ############################################## 317 | # Unittest 318 | 319 | import unittest 320 | 321 | class TestArithmetic(unittest.TestCase): 322 | 323 | def testAdd(self): 324 | self.assertEqual(add(1, 1), 2) 325 | 326 | def test_subtract(self): 327 | self.assertEqual(subtract(5, 2), 3) 328 | 329 | def test_multiply(self): 330 | self.assertEqual(multiply(3, 2), 6) 331 | 332 | def test_divide(self): 333 | self.assertEqual(divide(3.0, 2), 1.5) 334 | ``` 335 | 336 | #### 手動測試 337 | 338 | ```shell 339 | $ python -m unittest -v Arithmetic.py 340 | testAdd (Arithmetic.TestArithmetic) ... ok 341 | test_divide (Arithmetic.TestArithmetic) ... ok 342 | test_multiply (Arithmetic.TestArithmetic) ... ok 343 | test_subtract (Arithmetic.TestArithmetic) ... ok 344 | 345 | ---------------------------------------------------------------------- 346 | Ran 4 tests in 0.003s 347 | 348 | OK 349 | ``` 350 | 351 | > 非常重要的原則:檔案進 SCM 前,一定要確定程式碼可以編譯成功,單元測試可以順利通過 352 | 353 | #### 新增檔案 .gitignore 354 | 355 | .gitignore 可以避免不必要的資料提交到 git repository,例如編譯中間檔、資料庫、密碼等等。目前先略過 Python cache data 356 | 357 | ``` 358 | __pycache__ 359 | ``` 360 | 361 | #### 提交程式碼 362 | 363 | ```shell 364 | $ git add . 365 | $ git commit -m "add Arithmetic with unittest" 366 | ``` 367 | 368 | ### 在 Jenkins Server 上 369 | 370 | #### 修改 Build Job 371 | 372 | - 到「myBuild」頁面,點選「Configure」 373 | - 「Build」下「Execute shell」,「Command」改成下面 shell script 374 | - 按下「Save」儲存離開 375 | 376 | ```shell 377 | #!/bin/bash 378 | python -m unittest -v *.py 379 | ``` 380 | 381 | 修改 myBuild Configure,不會觸發 Build Job,手動執行後觀察結果 382 | 383 | - 到「myBuild」頁面,點選「Build Now」 384 | - 看到「Build History」出現 Build item,點選最新的 Build result 385 | - 點選「Console Output」,看到以下 Build process 386 | 387 | ``` 388 | ... (略) 389 | testAdd (Arithmetic.TestArithmetic) ... ok 390 | test_divide (Arithmetic.TestArithmetic) ... ok 391 | test_multiply (Arithmetic.TestArithmetic) ... ok 392 | test_subtract (Arithmetic.TestArithmetic) ... ok 393 | 394 | ---------------------------------------------------------------------- 395 | Ran 4 tests in 0.000s 396 | 397 | OK 398 | Hello World 399 | Finished: SUCCESS 400 | ``` 401 | 402 | 使用 `python -m unittest *.py` 執行單元測試太過土砲,接下來改用正式的執行方式,順便導入一些靜態檢查的工具。 403 | 404 | ## 實驗五:自動化測試工具 405 | 406 | ### 練習目標 407 | 408 | - 在開發環境上 409 | - 使用 pip 安裝 [coverage](http://nedbatchelder.com/code/coverage/), [nose](https://nose.readthedocs.org/), [pylint](http://www.pylint.org/) 套件 410 | - 使用 coverage 產生程式碼覆蓋率的分析報告 411 | - 使用 nosetest 運行單元測試 412 | - 使用 pylint 產生程式碼質量的分析報告 413 | - 提交 Python 套件相依清單到 git repository 414 | 415 | - 在 Jenkins Server 上 416 | - 以 jenkins 使用者身份安裝 virtualenv 417 | - 設定自動化測試工具 418 | 419 | ### 在開發環境上 420 | 421 | #### 安裝套件 422 | 423 | ```shell 424 | $ pip install coverage nose pylint 425 | ``` 426 | 427 | #### 運行單元測試 428 | 429 | ```shell 430 | $ nosetests --with-xunit --all-modules --traverse-namespace --with-coverage --cover-package=. --cover-inclusive 431 | .... 432 | Name Stmts Miss Cover 433 | ----------------------------------- 434 | Arithmetic.py 18 0 100% 435 | HelloWorld.py 1 0 100% 436 | ----------------------------------- 437 | TOTAL 19 0 100% 438 | ---------------------------------------------------------------------- 439 | Ran 4 tests in 0.009s 440 | 441 | OK 442 | ``` 443 | - 產生分析報告 nosetests.xml 444 | 445 | #### 檢查程式碼覆蓋率 446 | 447 | ```shell 448 | $ python -m coverage xml --include=* 449 | ``` 450 | - 產生分析報告 coverage.xml 451 | 452 | #### 檢查程式碼質量 453 | 454 | ```shell 455 | $ pylint -f parseable *.py | tee pylint.out 456 | ...(略) 457 | 458 | Report 459 | ====== 460 | 19 statements analysed. 461 | 462 | Statistics by type 463 | ------------------ 464 | 465 | +---------+-------+-----------+-----------+------------+---------+ 466 | |type |number |old number |difference |%documented |%badname | 467 | +=========+=======+===========+===========+============+=========+ 468 | |module |2 |2 |= |0.00 |0.00 | 469 | +---------+-------+-----------+-----------+------------+---------+ 470 | |class |1 |1 |= |0.00 |0.00 | 471 | +---------+-------+-----------+-----------+------------+---------+ 472 | |method |4 |4 |= |0.00 |25.00 | 473 | +---------+-------+-----------+-----------+------------+---------+ 474 | |function |4 |4 |= |0.00 |0.00 | 475 | +---------+-------+-----------+-----------+------------+---------+ 476 | 477 | ...(略) 478 | ``` 479 | - 產生分析報告 pylint.out 480 | 481 | #### 匯出套件相依列表 482 | 483 | ```shell 484 | $ pip freeze > requirements.txt 485 | ``` 486 | 487 | requirements.txt 內容: 488 | ``` 489 | astroid==1.4.6 490 | colorama==0.3.7 491 | coverage==4.1 492 | lazy-object-proxy==1.2.2 493 | nose==1.3.7 494 | pylint==1.5.6 495 | six==1.10.0 496 | wrapt==1.10.8 497 | ``` 498 | 499 | #### 修改 .gitignore 500 | 501 | ``` 502 | __pycache__ 503 | .coverage 504 | coverage.xml 505 | nosetests.xml 506 | pylint.out 507 | ``` 508 | 509 | #### 提交相依列表 510 | 511 | ```shell 512 | $ git add . 513 | $ git commit -m "add requirements for pip" 514 | ``` 515 | ### 在 Jenkins Server 上 516 | 517 | 要在 Jenkins Server 執行開發環境相同的任務 ([coverage](http://nedbatchelder.com/code/coverage/), [nose](https://nose.readthedocs.org/), [pylint](http://www.pylint.org/)),也要安裝相同的虛擬環境。 518 | 519 | #### 設定 jenkins 環境 520 | 521 | - 以 jenkins 身份登入系統 522 | - 設定環境,[安裝 pyenv、virtualenv](environment.md#安裝-pyenvvirtualenv) 523 | 524 | ```shell 525 | $ sudo su jenkins -l 526 | $ git clone https://github.com/yyuu/pyenv.git ~/.pyenv 527 | $ git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv 528 | ``` 529 | 530 | #### 修改 Build Job 531 | 532 | - 到「myBuild」頁面,點選「Configure」 533 | - 「Build」下「Execute shell」,「Command」改成下面 shell script 534 | - 按下「Save」儲存離開 535 | 536 | ```shell 537 | PATH=$WORKSPACE/venv/bin:/usr/local/bin:$PATH 538 | 539 | nosetests --with-xunit --all-modules --traverse-namespace --with-coverage --cover-package=. --cover-inclusive 540 | python -m coverage xml --include=* 541 | pylint -f parseable *.py | tee pylint.out 542 | ``` 543 | 544 | - 到「myBuild」頁面,點選「Configure」 545 | - 「Build」內按下「Add build step」,選擇「Execute shell」,「Command」填入下面 shell script 546 | - 將這個 「Execute shell」移動到「Build」區塊最上面 547 | - 按下「Save」儲存離開 548 | 549 | ```shell 550 | PATH=$WORKSPACE/venv/bin:/usr/local/bin:$PATH 551 | 552 | if [ ! -d "venv" ]; then 553 | virtualenv venv 554 | fi 555 | . venv/bin/activate 556 | pip install -r requirements.txt 557 | ``` 558 | 559 | > 上面這段 script 主要目的是設定 python 虛擬環境,並且安裝開發環境需要的 python 套件 560 | 561 | 修改 myBuild Configure,不會觸發 Build Job,手動執行後觀察結果 562 | 563 | - 到「myBuild」頁面,點選「Build Now」 564 | - 看到「Build History」出現 Build item,點選最新的 Build result 565 | - 點選「Console Output」,看到以下 Build process 566 | 567 | Jenkins Console Output 的內容就是手動執行的結果,雖然詳細但可讀性非常差。接下來在 Jenkins Server 上加入一些 plugin,讓結果可以用圖形方式呈現。 568 | 569 | ## 實驗六:圖形化測試結果 570 | 571 | ### 練習目標 572 | 573 | - 在 Jenkins Server 上 574 | - 安裝 [Cobertura plugin](https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin) (用来顯示程式碼覆蓋率) 575 | - 安裝 [Violations plugin](https://wiki.jenkins-ci.org/display/JENKINS/Violations) (用來顯示程式碼是否合乎規範) 576 | - 設定 Post-build Actions,產生圖形結果 577 | 578 | ### 在 Jenkins Server 上 579 | 580 | #### 安裝 Cobertura plugin 581 | 582 | - 到 Jenkins 首頁,選擇「Manage Jenkins」 583 | - 點選「Manage Plugins」,進入設定插件管理頁面 584 | - 選擇「Available」標籤,「filter」輸入 `Cobertura plugin` 585 | - 選取「Cobertura plugin」,按下「Install without restart」 586 | - 等候安裝完成 587 | 588 | #### 安裝 Violations plugin 589 | 590 | - 到 Jenkins 首頁,選擇「Manage Jenkins」 591 | - 點選「Manage Plugins」,進入設定插件管理頁面 592 | - 選擇「Available」標籤,「filter」輸入 `Violations plugin` 593 | - 選取「Violations plugin」,按下「Install without restart」 594 | - 等候安裝完成 595 | 596 | #### 修改 Build Job 597 | 598 | - 到「myBuild」頁面,點選「Build Now」 599 | - 「Post-build Actions」下點選「Publish Cobertura Coverage Report」,「Cobertura xml report pattern」填入 `coverage.xml` 600 | - 「Post-build Actions」下點選「Publish JUnit test result report」,「Test report XMLs」填入 `nosetests.xml` 601 | - 「Post-build Actions」下點選「Report Violations」,「pylint」填入 `pylint.out` 602 | - 按下「Save」儲存離開 603 | 604 | 修改 myBuild Configure,不會觸發 Build Job,手動執行後觀察結果 605 | 606 | - 到「myBuild」頁面,點選「Build Now」 607 | - 看到「Build History」出現 Build item,點選最新的 Build result 608 | - 點選「Console Output」,看到以下 Build process 609 | - 到「myBuild」頁面,點選「Coverage Report」,觀察圖形分析報告 610 | - 到「myBuild」頁面,點選「Violations」,觀察圖形分析報告 611 | 612 | ![Jenkins myBuild](jenkins-myBuild.png) 613 | 614 | ---- 615 | ## 參考 616 | - [Using Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Use+Jenkins) - Jenkins 官網文件 617 | - [CI (Continuous integration) 關鍵技術:使用 Jenkins](http://www.books.com.tw/products/0010596579) - 偏重 Android App 開發 618 | - [持續整合與自動化測試 - 使用 Jenkins 與 Docker 進行課程實作](https://www.gitbook.com/book/smlsunxie/jenkins-workshop/details) - 內容較完整 619 | - [Jenkins CI 實戰手冊](http://jenkins.readbook.tw/) - 內容不完整 620 | - [Jenkins CI 從入門到實戰講座](http://trunk-studio.kktix.cc/events/jenkins-2016001) - 送電子書,內有詳盡的實作步驟說明 621 | - [Python Projects](https://wiki.jenkins-ci.org/display/JENKINS/Python+Projects) - Using Jenkins for Python Projects 622 | - Automated python unit testing, code coverage and code quality analysis with Jenkins - [part 1](http://bhfsteve.blogspot.tw/2012/04/automated-python-unit-testing-code.html), [part 2](http://bhfsteve.blogspot.tw/2012/04/automated-python-unit-testing-code_20.html), [part 3](http://bhfsteve.blogspot.tw/2012/04/automated-python-unit-testing-code_27.html) 623 | - [基于 Jenkins 的 Python 代码集成整合](http://yumminhuang.github.io/blog/2015/04/17/%E5%9F%BA%E4%BA%8E-jenkins-%E7%9A%84-python-%E4%BB%A3%E7%A0%81%E9%9B%86%E6%88%90%E6%95%B4%E5%90%88/) 624 | - [jenkins集成python的单元测试](http://www.mamicode.com/info-detail-1383168.html) 625 | - [Making Jenkins Work With Python's Virtualenv](http://iamnearlythere.com/jenkins-python-virtualenv/) 626 | -------------------------------------------------------------------------------- /django.md: -------------------------------------------------------------------------------- 1 | # Django 2 | 3 | 雖然 Django 跟 BDD/TDD 沒有直接的關係,但為了開發流程的完整概念,我嘗試把 Django 這個 web framework 安裝起來,然後在上面開發與測試。 4 | 5 | 練習過程以這份 [Django Tutorial](http://daikeren.github.io/django_tutorial/) 為主,但因為版本關係有些使用方式太舊,參考了另一份 [Django Girls 學習指南](https://www.gitbook.com/book/djangogirlstaipei/django-girls-taipei-tutorial/details) 作為輔助。 6 | 7 | > 本來想練習 Django 官網提供的土托魚 Writing your first Django app, part [1](https://docs.djangoproject.com/en/1.9/intro/tutorial01/), [2](https://docs.djangoproject.com/en/1.9/intro/tutorial02/), [3](https://docs.djangoproject.com/en/1.9/intro/tutorial03/), [4](https://docs.djangoproject.com/en/1.9/intro/tutorial04/), [5](https://docs.djangoproject.com/en/1.9/intro/tutorial05/), [6](https://docs.djangoproject.com/en/1.9/intro/tutorial06/), [7](https://docs.djangoproject.com/en/1.9/intro/tutorial07/),但這一系列鉅細彌遺的讓人有點吃不消 (我只想快速上手啊,摔筆),只好改看其他人消化吸收過的東西。 8 | 9 | Let's 開始練功吧! 10 | 11 | ## 簡介 12 | 13 | 世界上除了 RoR 之外,還有其他 Web Framework,像是 Node.js 以及 Django。 14 | 15 | 如同 Python 生態環境之豐富,Django 也有很多如資料庫、表單、登入系統、管理界面等元件,大大縮短了後端開發的時間。 16 | 17 | ## 設定環境 18 | 19 | 安裝與管理套件版本是軟體開發頗讓人頭疼的一環,幸好靠著 pyenv 管理多個 Python 版本問題,與 virtualenv 創造虛擬(獨立)Python 環境的工具,Python 工程師的生活才能過得輕鬆點。 20 | 21 | ### 安裝 pyenv 與 virtualenv 22 | ```shell 23 | $ git clone https://github.com/yyuu/pyenv.git ~/.pyenv 24 | $ git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv 25 | $ sudo pip install virtualenv 26 | ``` 27 | 28 | 設定 ~/.bashrc 29 | ``` 30 | export PYENV_ROOT="$HOME/.pyenv" 31 | export PATH="$PYENV_ROOT/bin:$PATH" 32 | eval "$(pyenv init -)" 33 | ``` 34 | 35 | 重新載入 ~/.bashrc 36 | ```shell 37 | $ source ~/.bashrc 38 | ``` 39 | 40 | ### 安裝 Python 3.5.1 41 | ```shell 42 | $ pyenv install 3.5.1 43 | ``` 44 | 45 | 設定 Python 3.5.1 46 | ```shell 47 | $ pyenv local 3.5.1 48 | $ python --version 49 | Python 3.5.1 50 | ``` 51 | 52 | ### 設定虛擬環境 53 | 54 | 建立工作目錄,設定虛擬環境,更新 pip (從此只要在 virtualenv 下面安裝的 package 都只會存在于這個 virtualenv 當中,安裝套件不需要 root 權限) 55 | 56 | ```shell 57 | $ mkdir myWorkspace 58 | $ cd myWorkspace/ 59 | $ pyvenv venv 60 | $ source venv/bin/activate 61 | (venv) $ pip install --upgrade pip 62 | ``` 63 | 64 | > 使用 `virtualenv` 安裝虛擬環境,跟使用 `pyvenv` 結果有點不同。`virtualenv` 似乎只會安裝系統預設的 python 版本,用 `pyvenv` 裝的 python 版本跟 `pyenv local 3.5.1` 才一致。 65 | 66 | ### 安裝 Django 67 | ```shell 68 | $ pip install Django==1.9.7 69 | ``` 70 | 71 | ## Django Project 72 | 73 | 建立 project 74 | ```shell 75 | $ django-admin.py startproject blog 76 | $ tree blog/ 77 | blog/ 78 | ├── blog 79 | │   ├── __init__.py 80 | │   ├── settings.py 81 | │   ├── urls.py 82 | │   └── wsgi.py 83 | └── manage.py 84 | ``` 85 | - 外層的 `blog/`: 包含整個專案的根目錄 86 | - 內層的 `blog/`: 放置專案相關的設定 87 | - `mysite/__init__.py`: 空檔,告訴 Python 把這個目錄當成 package 88 | - `mysite/settings.py`: 專案設定檔 89 | - `mysite/urls.py`: URL 定義檔,用來告訴 Django 怎麼發送 URL request 90 | - `mysite/wsgi.py`: 用來執行專案的 WSGI-compatible web servers 進入點 (將來上雲端才會用到) 91 | - `manage.py`: 命令列工具,用來操作專案 92 | 93 | 進入外層 `blog/` (之後都在這裡開發應用程式),啟動服務 94 | ```shell 95 | $ cd blog/ 96 | $ python manage.py runserver 0.0.0.0:8000 97 | ``` 98 | 99 | 開啟瀏覽器,連接 `http://192.168.33.10:8000/` ([虛擬機](environment.md)),看到以下訊息 100 | ``` 101 | It worked! 102 | Congratulations on your first Django-powered page. 103 | ``` 104 | 105 | ## Django APPs 106 | 107 | - Django Project 由多個 Django APP 組成,每個 Django App 可以被多個 Django Project 所使用 108 | - Django App 就是一個功能單純的應用 109 | 110 | 創建 Django App 111 | ```shell 112 | $ python manage.py startapp article 113 | $ tree article/ 114 | article/ 115 | ├── admin.py 116 | ├── apps.py 117 | ├── __init__.py 118 | ├── migrations 119 | │   └── __init__.py 120 | ├── models.py 121 | ├── tests.py 122 | └── views.py 123 | ``` 124 | 125 | 讓 project 知道多了這個 app,編輯 `blog/settings.py` 126 | ``` 127 | INSTALLED_APPS = [ 128 | 'django.contrib.admin', 129 | 'django.contrib.auth', 130 | 'django.contrib.contenttypes', 131 | 'django.contrib.sessions', 132 | 'django.contrib.messages', 133 | 'django.contrib.staticfiles', 134 | 'article', 135 | ] 136 | ``` 137 | - 在 INSTALLED_APPS 內新增 `article` 138 | 139 | ## Django Views & Django URLs 140 | 141 | Django 不採取 MVC 架構,而是 MTV 架構。操作 View 與 URL 之前,先由下圖了解 URL Request 被如何處理,然後變成使用者在瀏覽器看到的結果。 142 | 143 | ![Python MTV model](http://1.bp.blogspot.com/-zsIAtTJ0aNg/VdgZlmmnADI/AAAAAAAA6A0/edFRO2N9Yb8/s1600/432038560_9f8b830dfe_o.png)(圖片出處 http://mropengate.blogspot.tw/2015/08/mvcdjangomtv.html) 144 | 145 | 1. URL 派送器 (urls.py) 將 URL Request 對應到 View 的函式 146 | 2. View 函式 (views.py) 執行 request 動作,通常引發讀寫資料庫的動作 147 | 3. Model (models.py) 定義資料庫的資料以及如何跟它互動的方式 148 | 4. 處理完 request 任務,View 就會回覆 HTTP Response 給瀏覽器 149 | 5. Templates 放置 HTML 檔案,這些檔案包含 HTML 語法與網頁如何呈現的邏輯 150 | 151 | 創建一個 View,修改 article/views.py,新增一下程式 152 | ```python 153 | from django.http import HttpResponse 154 | 155 | def home(request): 156 | s = "Hello World!" 157 | return HttpResponse(s) 158 | ``` 159 | - 沒有涉及底層 Model,僅僅回覆 "Hello World!" 訊息 160 | 161 | 告訴 Django 要如何處理 URL,修改 blog/urls.py,在 `urlpatterns` 加入下面程式 162 | ```python 163 | urlpatterns = [ 164 | ... 165 | url(r'^$', 'article.views.home'), 166 | ] 167 | ``` 168 | 169 | 重啟服務,連接 `http://192.168.33.10:8000/`,看到以下訊息 170 | ``` 171 | Hello World! 172 | ``` 173 | 174 | ## Django Model 175 | 176 | Django 支援許多資料庫,只要簡單設定就能存取 sqlite、PostgreSQL、MySQL 與 Oracle。 177 | 178 | 與資料庫相關設定放在 blog/settings.py,來看看 179 | ```python 180 | DATABASES = { 181 | 'default': { 182 | 'ENGINE': 'django.db.backends.sqlite3', 183 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 184 | } 185 | } 186 | ``` 187 | - `ENGINE` 採用 sqlite3 188 | - `NAME` 宣告資料庫檔案位置在 `blog/db.sqlite3` 189 | 190 | Django 中透過 Model 來處理與底層資料庫的互動,要在 Model 定義表格的存取方式 191 | 192 | - Model 繼承 `django.db.models.Model` 193 | - Model 的屬性 (attribute) 都是一個資料庫的欄位 194 | - 透過 Model API 執行資料庫 query,將每種資料庫 SQL 版本間的差異隱藏起來 195 | 196 | 設定 blog/article 的資料 Model,修改 articles/models.py,加入以下程式碼 197 | ```python 198 | from django.db import models 199 | 200 | # Create your models here. 201 | class Category(models.Model): 202 | name = models.CharField(u'Name', max_length=50) 203 | 204 | def __str__(self): 205 | return self.name 206 | 207 | class Article(models.Model): 208 | content = models.TextField(u'Content') 209 | title = models.CharField(u'Title', max_length=50) 210 | category = models.ForeignKey('Category', blank=True, null=True) 211 | 212 | def __str__(self): 213 | return self.title 214 | ``` 215 | - 建立兩個 Model: Category & Article 216 | - Category 宣告一個屬性: `name`,並定義 `__str__` 用字串表示自己 217 | - Article 宣告三個屬性: `content` 表示文章內容, `title` 表示文章標題, `category` 使用 `ForeignKey` 定義資料表格間的關聯性 218 | 219 | 剛剛只是定義 Model,資料庫的表格還沒建立起來,要透過以下指令產生 220 | ```shell 221 | $ python manage.py makemigrations 222 | $ python manage.py migrate 223 | ``` 224 | 225 | 現在透過 Django 提供的 shell 來看看資料庫表格的樣子 226 | ``` 227 | $ python manage.py shell 228 | >>> 229 | ``` 230 | - 看到 `>>>` 提示符號表示進入 Python REPL 231 | 232 | 試試創建三筆記錄 233 | ```python 234 | >>> from article.models import Article, Category 235 | >>> Article.objects.create(content="Test1", title="article 1") 236 | 237 | >>> Article.objects.create(content="Test2", title="article 2") 238 | 239 | >>> c = Category.objects.create(name="category 1") 240 | >>> Article.objects.create(content="Test3", title="article 3", category=c) 241 | 242 | >>> 243 | ``` 244 | 245 | 查詢剛剛插入的資料 246 | ```python 247 | >>> Article.objects.all() 248 | [, , , ] 249 | >>> for article in Article.objects.all(): 250 | ... print(article.title) 251 | ... 252 | article 1 253 | article 2 254 | article 3 255 | ``` 256 | 257 | 修改一筆記錄 258 | ```python 259 | >>> from article.models import Article, Category 260 | >>> a = Article.objects.get(title="article 1") 261 | >>> a.title = "Article" 262 | >>> a.save() 263 | ``` 264 | 265 | 操作完畢,使用 Ctrl-D 離開 Python REPL 266 | 267 | Django Model 幫忙處理了與資料庫互動的部分,我很好奇它做了什麼,以下透過 sqlite 直接下 SQL 來看看 268 | ```shell 269 | $ sqlite3 db.sqlite3 270 | ``` 271 | 272 | 透過 `.table` 命令列出所有表格 273 | ```mysql 274 | sqlite> .tables 275 | article_article auth_user_groups 276 | article_category auth_user_user_permissions 277 | auth_group django_admin_log 278 | auth_group_permissions django_content_type 279 | auth_permission django_migrations 280 | auth_user django_session 281 | ``` 282 | - `article_article` 對應到 article/models.py 裡面的 `class Article(models.Model)` 283 | - `article_category` 對應到 article/models.py 裡面的 `class Category(models.Model)` 284 | 285 | 使用 `.schema` 顯示表格 schema 286 | ```sql 287 | sqlite> .schema article_article 288 | CREATE TABLE "article_article" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "content" text NOT NULL, "title" varchar(50) NOT NULL, "category_id" integer NULL REFERENCES "article_category" ("id")); 289 | CREATE INDEX "article_article_b583a629" ON "article_article" ("category_id"); 290 | sqlite> .schema article_category 291 | CREATE TABLE "article_category" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(50) NOT NULL); 292 | ``` 293 | - 除了原先設定的表格,Django Model 還透過 `id` 維護一個自動增加的唯一主鍵 294 | 295 | 查詢 `article_article` 與 `article_category` 表格內容 296 | ```sql 297 | sqlite> .header on 298 | sqlite> .mode column 299 | sqlite> SELECT * FROM article_article; 300 | id content title category_id 301 | ---------- ---------- ---------- ----------- 302 | 1 Test1 Article 303 | 2 Test2 article 2 304 | 3 Test3 article 3 1 305 | sqlite> SELECT * FROM article_category; 306 | id name 307 | ---------- ---------- 308 | 1 category 1 309 | ``` 310 | 311 | 不做任何修改,使用 `.quit` 離開 312 | ```sql 313 | sqlite> .quit 314 | ``` 315 | 316 | ## Django Admin 317 | 318 | 除了可以透過 Shell 操作 Model,Django 提供了 admin app 讓使用者可以透過 web UI 做創造、讀取、更新、刪除的動作。 319 | 320 | Django 預設安裝 admin,打開 blog/settings.py 查看 321 | ```python 322 | INSTALLED_APPS = [ 323 | 'django.contrib.admin', 324 | ... 325 | ] 326 | ``` 327 | 328 | URL 導向設定在 blog/urls.py 329 | ```python 330 | urlpatterns = [ 331 | url(r'^admin/', admin.site.urls), 332 | ... 333 | ] 334 | ``` 335 | 336 | 修改 article/admin.py,告訴 Django 建立了 Category, Article 這兩個 Model 337 | ```python 338 | from django.contrib import admin 339 | from article.models import Article, Category 340 | 341 | # Register your models here. 342 | admin.site.register(Article) 343 | admin.site.register(Category) 344 | ``` 345 | 346 | 設定 superuser 帳號 347 | ```shell 348 | $ python manage.py createsuperuser 349 | Username (leave blank to use 'vagrant'): django 350 | Email address: printk@gmail.com 351 | Password: 352 | Password (again): 353 | Superuser created successfully. 354 | You have mail in /var/mail/vagrant 355 | ``` 356 | 357 | 設定完成,啟動服務,登入 http://192.168.33.10:8000/admin/,透過 Web UI 新增、查詢、修改、刪除記錄。 358 | 359 | ## Django Views & Django URL 再訪 360 | 361 | 想辦法做出讓網站可以根據 URL 動態產生網頁,例如 URL=`article/1/` 呈顯第一則文章 362 | 363 | 先從 view 開始,修改 article/views.py,加上下面程式碼 364 | ```python 365 | from article.models import Article 366 | 367 | def detail(request, pk): 368 | article = Article.objects.get(pk=int(pk)) 369 | s = """ 370 | 371 | 372 | 373 |

{0}

374 | {1} 375 | 376 | 377 | """.format(article.title, article.content) 378 | return HttpResponse(s) 379 | ``` 380 | - `detail` 根據參數 `pk` 查詢文章的標題與內容,插入 HTML 中,然後回覆給瀏覽器 381 | 382 | 接著,修該 blog/urls.py,讓 Django 能夠解析 URL Request,得到要給 `detail` 的 `pk` 383 | ```python 384 | urlpatterns = [ 385 | ... 386 | url(r'^article/(?P[0-9]+)/$', 'article.views.detail'), 387 | ] 388 | ``` 389 | - `(?P[0-9]+)` 解析 URL=`article/1` 後面的數字,得到 pk 這個 group name 然後傳給 `detail` 390 | - [Named Group](https://docs.djangoproject.com/ja/1.9/topics/http/urls/#named-groups): In Python regular expressions, the syntax for named regular-expression groups is `(?Ppattern)`, where `name` is the name of the group and `pattern` is some pattern to match. 391 | 392 | 設定完成,啟動服務,打開 http://192.168.33.10:8000/article/1 ,看到第一篇文章的標題與內容。 393 | ``` 394 | Article 395 | 396 | Test1 397 | ``` 398 | 399 | ## Django Template 400 | 401 | 剛剛把 HTML 放在 View 會讓邏輯控制與網頁呈現的程式碼交錯混合,增加了前後端工程的複雜度。接下來要將 HTML 的提到 Templates 目錄中,讓前端網頁設計師處理 HTML 時不需要知道後端操作的邏輯。 402 | 403 | 在 article 目錄創建 templates 目錄,裡面新增一個檔案 article/templates/detail.html 404 | ```html 405 | 406 | 407 | 408 |

{{ article.title }}

409 | {{ article.content }} 410 | 411 | 412 | ``` 413 | - Django 定義許多 [template languate](https://docs.djangoproject.com/en/1.9/ref/templates/language/),方便前端工程師存取後端的變數 414 | - 這個 HTML 預期被 View 呼叫時會得到 `article` 物件,藉由 `{{ article.title }}` 與 `{{ article.content }}` 取得文章的標題與內容 415 | 416 | 修改之前的 View,article/views.py 417 | ```python 418 | def detail(request, pk): 419 | article = Article.objects.get(pk=int(pk)) 420 | return render(request, 'detail.html', {'article': article}) 421 | ``` 422 | - 透過 python dictionary 傳遞 `article` 變數給 template 423 | 424 | 除了簡單的變數取代,還可以使用 [if](https://docs.djangoproject.com/en/1.9/ref/templates/builtins/#std:templatetag-if) tag 與 [up](https://docs.djangoproject.com/en/1.9/ref/templates/builtins/#upper) filter 增加 template 的變化。修改 article/templates/detail.html 425 | ```html 426 | 427 | 428 | 429 |

{{ article.title }}

430 | {% if article.pk == 1 %} 431 | {{ article.content|upper }} 432 | {% else %} 433 | {{ article.content }} 434 | {% endif %} 435 | 436 | 437 | ``` 438 | 439 | 設定完成,啟動服務,打開 http://192.168.33.10:8000/article/1 ,看到第一篇文章的內容是否變成大寫。 440 | ``` 441 | Article 442 | 443 | TEST1 444 | ``` 445 | 446 | ## Django Forms 447 | 448 | 除了透過 Django Admin 修改資料,還可以手動產生自己的介面。 449 | 450 | 傳統 framework 會這麼做: 451 | 452 | - 用 HTML 刻個 form 453 | - 處理 HTTP POST request 454 | - 檢查表單欄位內容是否正確 455 | - 把確認過的資料存進 database 當中 456 | 457 | Django Form 提供另一種方便的方式,產生表單對應相對 Model 欄位,提供讀取、修改的功能。 458 | 459 | 先在 article/views.py 產生一個表單類別 460 | ```python 461 | from django import forms 462 | 463 | class ArticleForm(forms.ModelForm): 464 | class Meta: 465 | model = Article 466 | fields = ['title', 'content', ] 467 | ``` 468 | 469 | 然後告訴 View 如何處理 ArticleForm 470 | ```python 471 | def create(request): 472 | if request.method == 'POST': 473 | form = ArticleForm(request.POST) 474 | if form.is_valid(): 475 | new_article = form.save() 476 | return HttpResponseRedirect('/article/' + str(new_article.pk)) 477 | 478 | form = ArticleForm() 479 | return render(request, 'create_article.html', {'form': form}) 480 | ``` 481 | - 如果 request 是 POST,表示使用者送出表單,則驗證資料合法後寫入資料庫,然後將網頁導向剛剛輸入的資料 482 | - 如果 request 不是 POST,表示使用者準備填寫表單,則送出 `create_article.html` 483 | 484 | 接者產生 article/templates/create_article.html,讓使用者可以填寫資料 485 | ```html 486 | 487 | 488 | 489 |
490 | {% csrf_token %} 491 | {{ form.as_p }} 492 | 493 |
494 | 495 | 496 | ``` 497 | - 透過 `form.as_p` 在 HTML 中產生表單 498 | - 使用 `csrf_token` 做到 [Cross Site Request Forgery protection](https://docs.djangoproject.com/en/dev/ref/contrib/csrf/),避免網站被攻擊 499 | 500 | > [[技術分享] Cross-site Request Forgery (Part 1)](http://cyrilwang.pixnet.net/blog/post/31813568-%5B%E6%8A%80%E8%A1%93%E5%88%86%E4%BA%AB%5D-cross-site-request-forgery-(part-1)): 簡單來說,CSRF 就是在使用者不知情的情況下,讓瀏覽器送出請求給目標網站以達攻擊目的。 對於 HTTP 協定有所了解的讀者,看到這句話可能會覺得很困惑。因為在預設的情況下,任何人只要知道 URL 與參數都可以對網站發出任何請求,如此說來不是所有的網站都會遭受 CSRF 的攻擊了嗎?可以說是,也可以說不是。因此嚴格來說,CSRF 通常指的是發生在使用者已經登入目標網站後,駭客利用受害者的身分來進行請求,如此一來不但可以獲得受害者的權限,而且在系統的相關紀錄中也很難發現可疑之處。 501 | 502 | 最後修改 blog/urls.py,把 URL request 與 View 串起來 503 | ```python 504 | urlpatterns = [ 505 | ... 506 | url(r'^create/$', 'article.views.create'), 507 | ] 508 | ``` 509 | 510 | 設定完成,啟動服務,打開 http://192.168.33.10:8000/create 產生一篇文章,送出表單看看是否被導向剛剛產生的文章。 511 | ``` 512 | hello world 513 | 514 | this is a test 515 | ``` 516 | 517 | ## Django 的第三方套件們 518 | 519 | 除了 Django 自帶的套件,[Django Packages](https://www.djangopackages.com/) 提供更多樣化的套件。 520 | 521 | 接下來試著安裝 django-grappelli,讓 admin 畫面變得更漂亮些。 522 | 523 | 安裝套件 524 | ```shell 525 | $ pip install django-grappelli 526 | ``` 527 | 528 | 告訴 Django 新增 app,修改 blog/settings.py 529 | ```python 530 | INSTALLED_APPS = [ 531 | 'grappelli', 532 | 'django.contrib.admin', 533 | ... 534 | ] 535 | ``` 536 | - 加入 grappelli 放在 django.contrib.admin 之前 537 | 538 | 修改 blog/urls.py,加上相對 URL 539 | ```python 540 | from django.conf.urls import include 541 | 542 | urlpatterns = [ 543 | url(r'^grappelli/', include('grappelli.urls')), 544 | url(r'^admin/', admin.site.urls), 545 | ... 546 | ] 547 | ``` 548 | > 實驗發現,似乎不需要修改 blog/urls.py 這部分,只要在 blog/settings.py 新增 'grappelli' 這個 app 即可 549 | 550 | 設定完成,啟動服務,打開 http://192.168.33.10:8000/admin 看看網頁是不是變漂亮了。 551 | 552 | ## Deploy 前置動作 553 | 554 | 目前所做的工作只能透過本機瀏覽器看到結果,想讓全世界看到你的作品就要部署到伺服器上。[Heroku](https://dashboard.heroku.com/) 提供免費額度的服務給一些小型網站,讓還沒獲利的網站可以免費營運。接下來設定部署環境準備將剛剛寫的程式放上雲端。 555 | 556 | 安裝部署需要的工具 557 | ```shell 558 | $ pip install dj-database-url gunicorn dj-static 559 | ``` 560 | 561 | 將虛擬環境套件的版本列出來,儲存在 requirements.txt 562 | ```shell 563 | $ pip freeze > requirements.txt 564 | ``` 565 | 566 | 建立 Procfile 檔案,告訴 Heroku 要如何啟動我們的應用 567 | ``` 568 | web: gunicorn --pythonpath blog blog.wsgi 569 | ``` 570 | 571 | 為了讓 Heroku 知道要用哪一個版本的 Python,新增 runtime.txt 572 | ``` 573 | python-3.5.1 574 | ``` 575 | 576 | 跟 blog/settings.py 不同,正式上線的環境透過 blog/production_settings.py 來設定 577 | ```python 578 | # Import all default settings. 579 | from .settings import * 580 | 581 | import dj_database_url 582 | DATABASES = { 583 | 'default': dj_database_url.config() 584 | } 585 | 586 | # Static asset configuration. 587 | STATIC_ROOT = 'staticfiles' 588 | 589 | # Honor the 'X-Forwarded-Proto' header for request.is_secure(). 590 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 591 | 592 | # Allow all host headers. 593 | ALLOWED_HOSTS = ['*'] 594 | 595 | # Turn off DEBUG mode. 596 | DEBUG = False 597 | ``` 598 | 599 | WSGI - Web Server Gateway Interface 是 Python 定義網頁程式和伺服器溝通的介面。為了讓 Heroku 的服務能夠透過 WSGI 介面與我們的網站溝通,修改 blog/wsgi.py 如下: 600 | ```python 601 | import os 602 | from django.core.wsgi import get_wsgi_application 603 | from dj_static import Cling 604 | 605 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") 606 | application = Cling(get_wsgi_application()) 607 | ``` 608 | - 將 dj_static 引入,並在 application 上使用它,以協助幫我們部署 static 檔案(例如圖片、CSS、JavaScript 檔案等等) 609 | 610 | 不想把開發時使用的檔案,例如虛擬環境、本機資料庫、Python cache等等放到網路上。建立一個 .gitignore 檔案,排除這些資料 611 | ``` 612 | *.pyc 613 | __pycache__ 614 | staticfiles 615 | db.sqlite3 616 | ``` 617 | 618 | ## Deploy to Heroku 619 | 620 | 在開始部署(deploy)之前 621 | 622 | 1. 註冊 Heroku 帳號:https://id.heroku.com/signup 623 | 2. 安裝 Heroku 工具箱:https://toolbelt.heroku.com 624 | 625 | 以虛擬機 (ARTACK/debian-jessie) 為例,安裝工具步驟如下 626 | ```shell 627 | $ wget -O- https://toolbelt.heroku.com/install-ubuntu.sh | sh 628 | ``` 629 | 630 | ### Step 1. 登入 Heroku 631 | 632 | ```shell 633 | $ heroku login 634 | ``` 635 | 636 | ### Step 2. 建立 git repository 637 | 638 | ```shell 639 | $ git init 640 | $ git add . 641 | $ git commit -m "my blog app" 642 | ``` 643 | 644 | ### Step 3-1: 新增新的 Heroku app 645 | 646 | 新增一個可以上傳 repository 的地方 647 | ```shell 648 | $ heroku create 649 | Heroku CLI submits usage information back to Heroku. If you would like to disable this, set `skip_analytics: true` in /home/vagrant/.heroku/config.json 650 | Creating app... done, ⬢ guarded-harbor-11820 651 | https://guarded-harbor-11820.herokuapp.com/ | https://git.heroku.com/guarded-harbor-11820.git 652 | ``` 653 | - 沒給 app 名稱,由 Heroku 隨機產生,得到 `guarded-harbor-11820` 654 | 655 | ### Step 3-2: 指定已經存在的 app 656 | 657 | 用指令 heroku apps 查看新增過 app 的名稱 658 | ```shell 659 | $ heroku apps 660 | === My Apps 661 | guarded-harbor-11820 662 | hidden-brook-77287 663 | ``` 664 | 665 | 設定成你想要上傳的 app 666 | ```shell 667 | $ heroku git:remote -a guarded-harbor-11820 668 | ``` 669 | 670 | 使用 `git remote` 指令檢查剛剛的設定 671 | ```shell 672 | $ git remote -v 673 | heroku https://git.heroku.com/guarded-harbor-11820.git (fetch) 674 | heroku https://git.heroku.com/guarded-harbor-11820.git (push) 675 | ``` 676 | 677 | ### Step 4: 設定環境變數 678 | 679 | ```shell 680 | $ heroku config:set DJANGO_SETTINGS_MODULE=blog.production_settings 681 | ``` 682 | 683 | ### Step 5: 利用 git push 上傳到 Heroku 684 | ```shell 685 | $ git push heroku master 686 | Counting objects: 24, done. 687 | Compressing objects: 100% (20/20), done. 688 | Writing objects: 100% (24/24), 5.12 KiB | 0 bytes/s, done. 689 | Total 24 (delta 0), reused 0 (delta 0) 690 | remote: Compressing source files... done. 691 | remote: Building source: 692 | remote: 693 | remote: -----> Python app detected 694 | remote: -----> Installing python-3.5.1 695 | remote: $ pip install -r requirements.txt 696 | ... 697 | ``` 698 | - python 版本依照 runtime.txt 安裝 python-3.5.1 699 | 700 | ### Step 6: 啟動 web process 701 | 702 | ```shell 703 | $ heroku ps:scale web=1 704 | ``` 705 | 706 | ### Step 7: Django project 初始化 707 | 708 | 進行資料庫初始化 709 | ```shell 710 | $ heroku run python manage.py migrate 711 | ``` 712 | 713 | 為新資料庫建立一個 superuser 714 | ```shell 715 | $ heroku run python manage.py createsuperuser 716 | ``` 717 | 718 | ### Step 8: 開啟瀏覽器觀看你的網站 719 | 720 | 透過 open 指令會自動在瀏覽器打開你的網站 721 | ```shell 722 | $ heroku open 723 | ``` 724 | 725 | 上面那招只能在 terminal console 跟 web browser 在同一個機器的情況下使用,因為我是透過虛擬環境開發的,要自行打開瀏覽器輸入剛剛 heroku 給的 app 名稱加上 “heroku.com” 才能連上,例如 https://guarded-harbor-11820.herokuapp.com/ 726 | 727 | ### Troubleshooting 728 | 729 | 參考: https://devcenter.heroku.com/articles/application-offline 730 | 731 | 查看 app 日誌訊息 732 | ```shell 733 | $ heroku logs 734 | ``` 735 | 736 | 檢查目前 process 狀態 737 | ```shell 738 | $ heroku ps 739 | ``` 740 | 741 | 顯示應用程式 dyno 使用狀態 742 | ```shell 743 | $ heroku scale web=1 744 | Scaling dynos... done, now running web at 1:Free 745 | ``` 746 | 747 | 如果掛掉,錯誤訊息會像這樣 748 | ```shell 749 | $ heroku ps 750 | === web (Free): gunicorn --pythonpath blog blog.wsgi (1) 751 | web.1: crashed 2016/06/10 12:00:30 +0200 (~ 17s ago) 752 | ``` 753 | 754 | 重新啟動看看能不能解決問題 755 | ```shell 756 | $ heroku restart 757 | ``` 758 | 759 | > learn-test/blog 裡面放的就是先前過做的事情,除了 “Deploy to Heroku” 這部分。想將這個 Project 部署到 Heroku,請先複製到 learn-test/ 以外的目錄 (因為 deploy 需要初始化 git repository,這會與 learn-test/.git 衝突),然後按照 Step1~8 完成部署動作。 760 | 761 | ## What’s next? 762 | 763 | 不管是 [Django Tutorial](http://daikeren.github.io/django_tutorial/) 還是 [Django Girls 學習指南](https://www.gitbook.com/book/djangogirlstaipei/django-girls-taipei-tutorial/details) 在文章最後都有一個 Todo list,可能是修改 template 使用 tag/filter 或套用 css/javascript,修改 Model 增加、刪除欄位,上傳檔案,增加留言功能等等。我的下一步是什麼呢? 764 | 765 | - 結合 Django 與 Behave,透過與 stakeholder 的對話逐步完成網站的功能 766 | - 使用 Python unittest 開發 View 或 Model 的功能 767 | - 使用 CI 做到測試自動化 768 | - 研究如何自動部署到 Heroku 769 | 770 | ---- 771 | ## 參考 772 | - [Django Tutorial](http://daikeren.github.io/django_tutorial/) 773 | - [Django Girls 學習指南](https://www.gitbook.com/book/djangogirlstaipei/django-girls-taipei-tutorial/details) 774 | - [Django documentation](https://docs.djangoproject.com/en/1.9/) 775 | -------------------------------------------------------------------------------- /experiment.md: -------------------------------------------------------------------------------- 1 | # 實驗 2 | 3 | ### 前言 4 | 5 | 這份筆記紀錄 TDD 與 BDD 的開發步驟,實驗意義大於真實開發所需要的步驟。過程中,筆記與程式碼 (在 test/ 目錄中) 幾經修修改改,除非你很有興趣想聽我碎碎念,不然建議不要看這篇 XD 6 | 7 | ### 準備工作 8 | 9 | 設定 mysql 環境 10 | ```mysql 11 | CREATE DATABASE `test`; 12 | USE `test`; 13 | 14 | CREATE TABLE `account` ( 15 | `id` int(11) NOT NULL AUTO_INCREMENT, 16 | `username` varchar(50) NOT NULL, 17 | `password` varchar(50) NOT NULL, 18 | PRIMARY KEY (`id`) 19 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; 20 | ``` 21 | 22 | ## BDD (Behavior-Driven Development) 23 | 24 | 目標:**溝通**帳號註冊與登入的邏輯 25 | 26 | 準備工作:產生專案目錄,建立 features 與 steps 目錄 27 | ```shell 28 | $ mkdir test 29 | $ cd test 30 | $ mkdir -p features/steps 31 | $ tree 32 | . 33 | └── features 34 | └── steps 35 | ``` 36 | 37 | ### 描述功能 (Feature) 38 | 39 | 在 features/account.feature 檔案一開頭,使用文字描述功能特性,讓創建規格的**產品經理**與實作功能的**程式設計師**在相同的 context 下進行溝通。 40 | 41 | ``` 42 | Feature: User account 43 | In order to buy or sell commodities 44 | As a buyer or seller 45 | I want to have a account in the Ecommerce website 46 | ``` 47 | 48 | ### 登入邏輯 49 | 50 | 在 features/account.feature 描述帳號登入的場景 51 | ``` 52 | Scenario: Login as correct username and password 53 | Given an username django with the password django123 is registered 54 | When I login as django and give the password django123 55 | Then I get the login result: successful 56 | 57 | Scenario: Login as incorrect username and password 58 | Given an username django with the password django123 is registered 59 | When I login as django and give the password abcdef123 60 | Then I get the login result: failed 61 | ``` 62 | 63 | 在什麼事情都沒做的情形下,執行 behave 會得到錯誤訊息,還有一些貼心的 snippet 64 | ```shell 65 | $ behave 66 | ...(略) 67 | 68 | 0 features passed, 1 failed, 0 skipped 69 | 0 scenarios passed, 2 failed, 0 skipped 70 | 0 steps passed, 0 failed, 0 skipped, 6 undefined 71 | Took 0m0.000s 72 | 73 | You can implement step definitions for undefined steps with these snippets: 74 | 75 | @given(u'an username django with the password django123 is registered') 76 | def step_impl(context): 77 | raise NotImplementedError(u'STEP: Given an username django with the password django123 is registered') 78 | 79 | @when(u'I login as django and give the password django123') 80 | def step_impl(context): 81 | raise NotImplementedError(u'STEP: When I login as django and give the password django123') 82 | 83 | @then(u'I get the login result: successful') 84 | def step_impl(context): 85 | raise NotImplementedError(u'STEP: Then I get the login result: successful') 86 | 87 | @when(u'I login as django and give the password abcdef123') 88 | def step_impl(context): 89 | raise NotImplementedError(u'STEP: When I login as django and give the password abcdef123') 90 | 91 | @then(u'I get the login result: failed') 92 | def step_impl(context): 93 | raise NotImplementedError(u'STEP: Then I get the login result: failed') 94 | ``` 95 | 96 | 修改上面的 snippet 產生跟應用程式介接的 steps,儲存在 features/steps/steps.py 97 | ```python 98 | @given(u'an username {username} with the password {password} is registered') 99 | def step_impl(context, username, password): 100 | raise NotImplementedError(u'STEP: Given an username {username} with the password {password} is registered') 101 | 102 | @when(u'I login as {username} and give the password {password}') 103 | def step_impl(context, username, password): 104 | raise NotImplementedError(u'STEP: When I login as {username} and give the password {password}') 105 | 106 | @then(u'I get the login result: {result}') 107 | def step_impl(context, result): 108 | raise NotImplementedError(u'STEP: Then I get the login result: {result}') 109 | ``` 110 | 111 | 再次執行 behave,得到下面結果 112 | ```shell 113 | Feature: User account # features/account.feature:1 114 | In order to buy or sell commodities 115 | As a buyer or seller 116 | I want to have a account in the Ecommerce website 117 | Scenario: Login as correct username and password # features/account.feature:6 118 | Given an username django with the password django123 is registered # features/steps/steps.py:1 0.000s 119 | Traceback (most recent call last): 120 | ...(略) 121 | NotImplementedError: STEP: Given an username {username} with the password {password} is registered 122 | 123 | When I login as django and give the password django123 # None 124 | Then I get the login result: successful # None 125 | 126 | Scenario: Login as incorrect username and password # features/account.feature:11 127 | Given an username django with the password django123 is registered # features/steps/steps.py:1 0.000s 128 | Traceback (most recent call last): 129 | ...(略) 130 | NotImplementedError: STEP: Given an username {username} with the password {password} is registered 131 | 132 | When I login as django and give the password abcdef123 # None 133 | Then I get the login result: failed # None 134 | 135 | 136 | Failing scenarios: 137 | features/account.feature:6 Login as correct username and password 138 | features/account.feature:11 Login as incorrect username and password 139 | 140 | 0 features passed, 1 failed, 0 skipped 141 | 0 scenarios passed, 2 failed, 0 skipped 142 | 0 steps passed, 2 failed, 4 skipped, 0 undefined 143 | Took 0m0.001s 144 | ``` 145 | - regular expression 解析 `{username}`, `{password}`, `{result}` 的部分ok, 146 | - 但因為功能尚未實作,得到測試錯誤的結果 (符合預期) 147 | 148 | ### 驗收測試 (Acceptance Test) 的意義 149 | 150 | ![Connecting the steps with the interface](connecting-the-steps-with-the-interface.png) 151 | 152 | 參考上圖 (請搭配下面的程式用力想像),steps.py 將 function.feature 裡面定義的 Given-When-Then 變成可執行的程式,但不是在這裡把功能做出來,而只是把上層的請求派送 (delegate) 到真正做事的地方 function.py。 153 | 154 | function.feature 155 | ``` 156 | Feature: function description 157 | As a , I want so that . 158 | 159 | Scenario: 160 | Given statement #1 161 | When statement #2 162 | Then statement #3 163 | ``` 164 | 165 | steps.py 166 | ```python 167 | @given(u'statement #1') 168 | def step_impl(context, ...): 169 | function1(...) 170 | 171 | @when(u'statement #2') 172 | def step_impl(context, ...): 173 | function2(...) 174 | 175 | @then(u'statement #3') 176 | def step_impl(context): 177 | assert(...) 178 | ``` 179 | 180 | function.py 181 | ```python 182 | def function1(...): 183 | # 真正做事情的地方 184 | 185 | def function2(...): 186 | # 真正做事情的地方 187 | ``` 188 | 189 | 接下來,我們會透過 TDD 的方式把底層功能實作出來。但真正開始實作功能之前,先思考介面(或是服務接口)如何設計。 190 | 191 | 回頭修改 features/steps/steps.py 192 | ```python 193 | @given(u'an username {username} with the password {password} is registered') 194 | def step_impl(context, username, password): 195 | account_insert(username, password) 196 | 197 | @when(u'I login as {username} and give the password {password}') 198 | def step_impl(context, username, password): 199 | context.result = "successful" if account_login(username, password) == True else "failed" 200 | 201 | @then(u'I get the login result: {result}') 202 | def step_impl(context, result): 203 | assert(context.result == result) 204 | ``` 205 | 206 | 執行 behave 207 | ```shell 208 | $ behave 209 | Feature: User account # features/account.feature:1 210 | In order to buy or sell commodities 211 | As a buyer or seller 212 | I want to have a account in the Ecommerce website 213 | Scenario: Login as correct username and password # features/account.feature:6 214 | Given an username django with the password django123 is registered # features/steps/steps.py:1 0.000s 215 | Traceback (most recent call last): 216 | ...(略) 217 | NameError: name 'account_insert' is not defined 218 | 219 | When I login as django and give the password django123 # None 220 | Then I get the login result: successful # None 221 | 222 | Scenario: Login as incorrect username and password # features/account.feature:11 223 | Given an username django with the password django123 is registered # features/steps/steps.py:1 0.000s 224 | Traceback (most recent call last): 225 | ...(略) 226 | NameError: name 'account_insert' is not defined 227 | 228 | When I login as django and give the password abcdef123 # None 229 | Then I get the login result: failed # None 230 | 231 | 232 | Failing scenarios: 233 | features/account.feature:6 Login as correct username and password 234 | features/account.feature:11 Login as incorrect username and password 235 | 236 | 0 features passed, 1 failed, 0 skipped 237 | 0 scenarios passed, 2 failed, 0 skipped 238 | 0 steps passed, 2 failed, 4 skipped, 0 undefined 239 | Took 0m0.000s 240 | ``` 241 | 242 | 還是執行失敗,但不同的是,錯誤訊息告訴我們 "NameError: name 'account_insert' is not defined" 有個函數沒有定義,接著就把它定義出來吧。 243 | 244 | 產生 features/steps/account.py,負責帳號管理 245 | ```python 246 | def account_insert(username, password): 247 | pass 248 | 249 | def account_login(username, password): 250 | pass 251 | 252 | def account_register(username, password): 253 | pass 254 | ``` 255 | 256 | 修改 features/steps/steps.py ,把 `form account import *` 加在最上面 257 | 258 | 執行 behave 259 | ```shell 260 | $ behave 261 | Feature: User account # features/account.feature:1 262 | In order to buy or sell commodities 263 | As a buyer or seller 264 | I want to have a account in the Ecommerce website 265 | Scenario: Login as correct username and password # features/account.feature:6 266 | Given an username django with the password django123 is registered # features/steps/steps.py:3 0.000s 267 | When I login as django and give the password django123 # features/steps/steps.py:7 0.000s 268 | Then I get the login result: successful # features/steps/steps.py:11 0.000s 269 | Traceback (most recent call last): 270 | ...(略) 271 | AssertionError 272 | 273 | 274 | Scenario: Login as incorrect username and password # features/account.feature:11 275 | Given an username django with the password django123 is registered # features/steps/steps.py:3 0.000s 276 | When I login as django and give the password abcdef123 # features/steps/steps.py:7 0.000s 277 | Then I get the login result: failed # features/steps/steps.py:11 0.000s 278 | 279 | 280 | Failing scenarios: 281 | features/account.feature:6 Login as correct username and password 282 | 283 | 0 features passed, 1 failed, 0 skipped 284 | 1 scenario passed, 1 failed, 0 skipped 285 | 5 steps passed, 1 failed, 0 skipped, 0 undefined 286 | Took 0m0.001s 287 | ``` 288 | 289 | 因為串接了底層的功能,場景 (Scenario) 的 `Given`, `When` 可以順利執行,但又因為沒有真的做什麼,所以 `Then` 驗證結果時發生錯誤 (第二個場景通過測試只是剛好條件符合而已,不是真的因為底層提供功能而通過)。 290 | 291 | 至此介面算是串接完成,我們先放下 BDD,來看看如何用 TDD 開發下層。 292 | 293 | ## TDD (Test-Driven Development) 294 | 295 | 雖然先有 interface,但接下來不是要實作功能,而是把單元測試補上 296 | 297 | 修改 features/steps/account.py,增加以下測試 298 | ```python 299 | import unittest 300 | class TestAccount(unittest.TestCase): 301 | 302 | @unittest.skip("need database mock") 303 | def test_account_insert(self): 304 | pass 305 | 306 | @unittest.skip("need database mock") 307 | def test_login_with_correct_username_password(self): 308 | pass 309 | 310 | @unittest.skip("need database mock") 311 | def test_login_with_invalid_username(self): 312 | pass 313 | 314 | @unittest.skip("need database mock") 315 | def test_login_with_invalid_password(self): 316 | pass 317 | 318 | @unittest.skip("need database mock") 319 | def test_register_with_valid_username_password(self): 320 | pass 321 | 322 | @unittest.skip("need database mock") 323 | def test_reigster_with_invalid_username(self): 324 | pass 325 | 326 | @unittest.skip("need database mock") 327 | def test_register_with_invalid_password(self): 328 | pass 329 | ``` 330 | 331 | account 需要跟資料庫互動才能完成 `account_insert`, `account_login`, `account_register` 的功能,但我想把這個依賴關係切開,先把測試標記為 skip 332 | 333 | 接下來不用 behave 進行測試,改用 `python -m unittest` 334 | ```shell 335 | $ python -m unittest -v account 336 | test_account_insert (account.TestAccount) ... skipped 'need database mock' 337 | test_login_with_correct_username_password (account.TestAccount) ... skipped 'need database mock' 338 | test_login_with_invalid_password (account.TestAccount) ... skipped 'need database mock' 339 | test_login_with_invalid_username (account.TestAccount) ... skipped 'need database mock' 340 | test_register_with_invalid_password (account.TestAccount) ... skipped 'need database mock' 341 | test_register_with_valid_username_password (account.TestAccount) ... skipped 'need database mock' 342 | test_reigster_with_invalid_username (account.TestAccount) ... skipped 'need database mock' 343 | 344 | ---------------------------------------------------------------------- 345 | Ran 7 tests in 0.005s 346 | 347 | OK (skipped=7) 348 | ``` 349 | 350 | 要切開與底層資料庫的依賴關係,要借用 unittest.mock 偽裝 database connector。先看看正常網路 MySQL-Connector 運作的方式,再決定如何偽造 351 | ```python 352 | import mysql.connector 353 | 354 | cnx = mysql.connector.connect(user='root', password='000000', host='127.0.0.1', database='test') 355 | cursor = cnx.cursor() 356 | 357 | query = "TRUNCATE TABLE account" 358 | cursor.execute(query) 359 | cnx.commit() 360 | 361 | query = "INSERT INTO account (username, password) VALUES ('abcdef', '123456')" 362 | cursor.execute(query) 363 | cnx.commit() 364 | 365 | query = "SELECT * FROM account WHERE username='%s'" % 'abcdef' 366 | cursor.execute(query) 367 | result = cursor.fetchone() 368 | print(result) 369 | 370 | cursor.close() 371 | cnx.close() 372 | ``` 373 | - 先不管 `mysql.connector.connect()`, `cnx.cursor()`, `cursor.close()`, `cnx.close()`,這些在 setup, tearDown 才會用到 374 | - `INSERT` 需要 `cursor.execute()` 與 `cnx.commit()` 375 | - `SELECT` 需要 `cursor.execute()` 與 `cursor.fetchone()` 376 | 377 | 藉由上面的範例,可以預期 `account_insert` 會呼叫到 `cursor.execute()` 與 `cnx.commit()`;而 `account_login` 會呼叫到 `cursor.execute()` 與 `cursor.fetchone()`。 378 | 379 | 加入 `from unittest.mock import Mock, patch`,移除 `@unittest.skip` 標記,修改單元測試 380 | ```python 381 | import unittest 382 | from unittest.mock import Mock, patch 383 | class TestAccount(unittest.TestCase): 384 | 385 | def setUp(self): 386 | self.result = None 387 | 388 | def test_account_insert(self): 389 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 390 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 391 | account_insert('abcdef', '123456') 392 | 393 | mock_execute.assert_called_with("INSERT INTO account (username, password) VALUES ('abcdef', '123456')") 394 | mock_commit.assert_called_with() 395 | 396 | def test_login_with_correct_username_password(self): 397 | def mock_execute(_, query): 398 | self.result = ('1',) if query == "SELECT id FROM account WHERE username='abcdef' AND password='123456'" else None 399 | def mock_fetchone(_): 400 | return self.result 401 | 402 | with patch('mysql.connector.cursor.MySQLCursor.execute', mock_execute): 403 | with patch('mysql.connector.cursor.MySQLCursor.fetchone', mock_fetchone): 404 | self.assertTrue(account_login('abcdef', '123456')) 405 | 406 | def test_login_with_invalid_username(self): 407 | def mock_execute(_, query): 408 | self.result = ('1',) if query == "SELECT id FROM account WHERE username='abcdef' AND password='123456'" else None 409 | def mock_fetchone(_): 410 | return self.result 411 | 412 | with patch('mysql.connector.cursor.MySQLCursor.execute', mock_execute): 413 | with patch('mysql.connector.cursor.MySQLCursor.fetchone', mock_fetchone): 414 | self.assertFalse(account_login('abc', '123456')) 415 | 416 | def test_login_with_invalid_password(self): 417 | def mock_execute(_, query): 418 | self.result = ('1',) if query == "SELECT id FROM account WHERE username='abcdef' AND password='123456'" else None 419 | def mock_fetchone(_): 420 | return self.result 421 | 422 | with patch('mysql.connector.cursor.MySQLCursor.execute', mock_execute): 423 | with patch('mysql.connector.cursor.MySQLCursor.fetchone', mock_fetchone): 424 | self.assertFalse(account_login('abcdef', '123')) 425 | 426 | def test_register_with_valid_username_password(self): 427 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 428 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 429 | self.assertTrue(account_register('abcdef', '123456')) 430 | 431 | self.assertTrue(mock_execute.called) 432 | self.assertTrue(mock_commit.called) 433 | 434 | def test_reigster_with_invalid_username(self): 435 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 436 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 437 | self.assertFalse(account_register('abc', '123456')) 438 | 439 | self.assertFalse(mock_execute.called) 440 | self.assertFalse(mock_commit.called) 441 | 442 | def test_register_with_invalid_password(self): 443 | with patch('mysql.connector.cursor.MySQLCursor.execute') as mock_execute: 444 | with patch('mysql.connector.connection.MySQLConnection.commit') as mock_commit: 445 | self.assertFalse(account_register('abcdef', '123')) 446 | ``` 447 | 448 | 很辛苦的單元測試... 449 | - `test_account_insert` patch 兩個 mysql.connector 的方法,呼叫 `account_insert('abcdef', '123456')`後,檢查 450 | - `cursor.execute()` 是否傳入 `"INSERT INTO account (username, password) VALUES ('abcdef', '123456')"` 451 | - `cnx.commit()` 是否被呼叫 452 | - `test_login_with_correct_username_password`, `test_login_with_invalid_username`, `test_login_with_invalid_password` patch 兩個 mysql.connector 的方法,然後呼叫 `account_login()` 453 | - `cursor.execute()` 接收 query,如果 query 是預期的則儲存 `('1',)` 否則儲存 `None` 454 | - `cursor.fetchone()` 回傳之前 query 儲存的結果 455 | - `test_register_with_valid_username_password`, `test_reigster_with_invalid_username`, `test_register_with_invalid_password` patch 兩個 mysql.connector 的方法 456 | - 如果 `username`, `password` 合法,回傳 `True`,否則回傳 `False` 457 | - 如果 `username`, `password` 合法,則 `cursor.execute()` 與 `cursor.fetchone()` 會被呼叫 458 | 459 | 有了單元測試,開時實作功能 460 | ```python 461 | import mysql.connector 462 | 463 | cnx = mysql.connector.connect(user='root', password='000000', host='127.0.0.1', database='test') 464 | cursor = cnx.cursor() 465 | 466 | def account_insert(username, password): 467 | query = "INSERT INTO account (username, password) VALUES ('%s', '%s')" % (username, password) 468 | cursor.execute(query) 469 | cnx.commit() 470 | 471 | def account_login(username, password): 472 | query = "SELECT id FROM account WHERE username='%s' AND password='%s'" % (username, password) 473 | cursor.execute(query) 474 | row = cursor.fetchone() 475 | return (row is not None) 476 | 477 | def account_register(username, password): 478 | if len(username) < 6 or len(password) < 6: 479 | return False 480 | account_insert(username, password) 481 | return True 482 | ``` 483 | 484 | 然後測試進行單元測試 485 | ```shell 486 | $ python -m unittest -v account 487 | test_account_insert (account.TestAccount) ... ok 488 | test_login_with_correct_username_password (account.TestAccount) ... ok 489 | test_login_with_invalid_password (account.TestAccount) ... ok 490 | test_login_with_invalid_username (account.TestAccount) ... ok 491 | test_register_with_invalid_password (account.TestAccount) ... ok 492 | test_register_with_valid_username_password (account.TestAccount) ... ok 493 | test_reigster_with_invalid_username (account.TestAccount) ... ok 494 | 495 | ---------------------------------------------------------------------- 496 | Ran 7 tests in 0.008s 497 | 498 | OK 499 | ``` 500 | 501 | 天呀~ 寫到這裡,發現這樣 TDD 真是太辛苦了!好在實務上 [django](https://www.djangoproject.com/) 藉由 Model 切開與資料庫的依賴關係,加上 [behave-django](https://pythonhosted.org/behave-django/) 提供更優良的 mock 技術,才能讓 Python 程式設計師的生活輕鬆點... 502 | 503 | 抱怨歸抱怨,既然頭已經洗下去,就繼續把它洗乾淨吧 XD 504 | 505 | ## 業務邏輯 506 | 507 | ### 驗證登入功能 508 | 509 | ```python 510 | $ behave 511 | Feature: User account # ../account.feature:1 512 | In order to buy or sell commodities 513 | As a buyer or seller 514 | I want to have a account in the Ecommerce website 515 | Scenario: Login as correct username and password # ../account.feature:6 516 | Given an username django with the password django123 is registered # steps.py:3 0.006s 517 | When I login as django and give the password django123 # steps.py:7 0.001s 518 | Then I get the login result: successful # steps.py:11 0.000s 519 | 520 | Scenario: Login as incorrect username and password # ../account.feature:11 521 | Given an username django with the password django123 is registered # steps.py:3 0.002s 522 | When I login as django and give the password abcdef123 # steps.py:7 0.001s 523 | Then I get the login result: failed # steps.py:11 0.000s 524 | 525 | 1 feature passed, 0 failed, 0 skipped 526 | 2 scenarios passed, 0 failed, 0 skipped 527 | 6 steps passed, 0 failed, 0 skipped, 0 undefined 528 | Took 0m0.010s 529 | ``` 530 | 531 | ### 新增註冊邏輯 532 | 533 | 在 features/account.feature 增加描述帳號登入的場景,這次使用 `Scenario Outline` 描述多個測試場景 534 | ``` 535 | Scenario Outline: username and password must be large than 5 characters 536 | When try to register a name with a password 537 | Then I get the register result: 538 | 539 | Examples: some usernames and passwords 540 | | username | password | result | 541 | | abc | 123456 | invalid username or password | 542 | | abcedf | 123 | invalid username or password | 543 | | abc | 123 | invalid username or password | 544 | | abcdef | 123456 | successful | 545 | ``` 546 | 547 | 在 features/steps/account.py 中,相對應的步驟如下 548 | ```python 549 | @when(u'try to register a name {username} with a password {password}') 550 | def step_impl(context, username, password): 551 | if account_register(username, password) == True: 552 | context.result = "successful" 553 | else: 554 | context.result = "invalid username or password" 555 | 556 | @then(u'I get the register result: {result}') 557 | def step_impl(context, result): 558 | assert(context.result == result) 559 | ``` 560 | - 使用 `{username}`, `{password}`, `{result}` 變數,不需要囉囉唆唆的針對每個場景寫 steps 561 | 562 | 執行 behave,得到下面有關註冊邏輯的驗證結果 563 | ```shell 564 | $ behave 565 | ...(略) 566 | 567 | Scenario Outline: username and password must be large than 5 characters -- @1.1 some usernames and passwords # account.feature:22 568 | When try to register a name abc with a password 123456 # steps/steps.py:18 0.000s 569 | Then I get the register result: invalid username or password # steps/steps.py:25 0.000s 570 | 571 | Scenario Outline: username and password must be large than 5 characters -- @1.2 some usernames and passwords # account.feature:23 572 | When try to register a name abcedf with a password 123 # steps/steps.py:18 0.001s 573 | Then I get the register result: invalid username or password # steps/steps.py:25 0.000s 574 | 575 | Scenario Outline: username and password must be large than 5 characters -- @1.3 some usernames and passwords # account.feature:24 576 | When try to register a name abc with a password 123 # steps/steps.py:18 0.000s 577 | Then I get the register result: invalid username or password # steps/steps.py:25 0.000s 578 | 579 | Scenario Outline: username and password must be large than 5 characters -- @1.4 some usernames and passwords # account.feature:25 580 | When try to register a name abcdef with a password 123456 # steps/steps.py:18 0.001s 581 | Then I get the register result: successful # steps/steps.py:25 0.000s 582 | 583 | ...(略) 584 | ``` 585 | 586 | ### 中文化 587 | 588 | behave 支援許多語系 589 | ```shell 590 | $ behave --lang-list 591 | Languages available: 592 | ar: العربية / Arabic 593 | bg: български / Bulgarian 594 | ca: català / Catalan 595 | cs: Česky / Czech 596 | cy-GB: Cymraeg / Welsh 597 | da: dansk / Danish 598 | de: Deutsch / German 599 | en: English / English 600 | en-Scouse: Scouse / Scouse 601 | en-au: Australian / Australian 602 | en-lol: LOLCAT / LOLCAT 603 | en-pirate: Pirate / Pirate 604 | en-tx: Texan / Texan 605 | eo: Esperanto / Esperanto 606 | es: español / Spanish 607 | et: eesti keel / Estonian 608 | fi: suomi / Finnish 609 | fr: français / French 610 | he: עברית / Hebrew 611 | hr: hrvatski / Croatian 612 | hu: magyar / Hungarian 613 | id: Bahasa Indonesia / Indonesian 614 | is: Íslenska / Icelandic 615 | it: italiano / Italian 616 | ja: 日本語 / Japanese 617 | ko: 한국어 / Korean 618 | lt: lietuvių kalba / Lithuanian 619 | lu: Lëtzebuergesch / Luxemburgish 620 | lv: latviešu / Latvian 621 | nl: Nederlands / Dutch 622 | no: norsk / Norwegian 623 | pl: polski / Polish 624 | pt: português / Portuguese 625 | ro: română / Romanian 626 | ru: русский / Russian 627 | sk: Slovensky / Slovak 628 | sr-Cyrl: Српски / Serbian 629 | sr-Latn: Srpski (Latinica) / Serbian (Latin) 630 | sv: Svenska / Swedish 631 | tr: Türkçe / Turkish 632 | uk: Українська / Ukrainian 633 | uz: Узбекча / Uzbek 634 | vi: Tiếng Việt / Vietnamese 635 | zh-CN: 简体中文 / Chinese simplified 636 | zh-TW: 繁體中文 / Chinese traditional 637 | ``` 638 | 639 | > python3.5 不支援 dict.sort(),要修改 /lib/python3.5/site-packages/behave/__main__.py:65: `iso_codes = sorted(iso_codes)#iso_codes.sort()` 640 | 641 | feature 可以寫成中文,只要文件第一行註記使用什麼語言,在 features/帳號.feature 642 | ``` 643 | # language: zh-TW 644 | 645 | 功能: 用戶帳號 646 | 為了買賣商品 647 | 身為買家或賣家 648 | 我想要有一個電子商務網站帳號 649 | 650 | 場景: 用正確的帳號跟密碼登入 651 | 假設< 帳號django與密碼django123已註冊 652 | 當< 我用django與密碼django123登入 653 | 那麼< 我得到登入結果:成功 654 | 655 | 場景: 用不正確的帳號跟密碼登入 656 | 假設< 帳號django與密碼django123已註冊 657 | 當< 我用django與密碼abcdef123登入 658 | 那麼< 我得到登入結果:失敗 659 | 660 | 場景大綱: 帳號與密碼必須大於5個字元 661 | 當< 嘗試用帳號與密碼註冊 662 | 那麼< 我得到註冊結果: 663 | 664 | 例子: 一些帳號與密碼 665 | | username | password | result | 666 | | abc | 123456 | 無效的帳號或密碼 | 667 | | abcedf | 123 | 無效的帳號或密碼 | 668 | | abc | 123 | 無效的帳號或密碼 | 669 | | abcdef | 123456 | 帳號建立 | 670 | ``` 671 | 672 | 步驟也可以用中文編寫,在 features/steps/步驟.py 673 | ```python 674 | from account import * 675 | 676 | @given(u'< 帳號{username}與密碼{password}已註冊') 677 | def step_impl(context, username, password): 678 | account_insert(username, password) 679 | 680 | @when(u'< 我用{username}與密碼{password}登入') 681 | def step_impl(context, username, password): 682 | if account_login(username, password) == True: 683 | context.result = "成功" 684 | else: 685 | context.result = "失敗" 686 | 687 | @then(u'< 我得到登入結果:{result}') 688 | def step_impl(context, result): 689 | assert(context.result == result) 690 | 691 | @when(u'< 嘗試用帳號{username}與密碼{password}註冊') 692 | def step_impl(context, username, password): 693 | if account_register(username, password) == True: 694 | context.result = "帳號建立" 695 | else: 696 | context.result = "無效的帳號或密碼" 697 | 698 | @then(u'< 我得到註冊結果:{result}') 699 | def step_impl(context, result): 700 | assert(context.result == result) 701 | ``` 702 | 703 | - features/account.feature 與 features/帳號.feature 做一樣的檢查 704 | - features/steps/step.py 與 features/steps/步驟.py 做一樣的事情 705 | 706 | 執行 behave,得到下面驗證結果 707 | ```shell 708 | $ mysql -uroot -p000000 -e 'truncate table account' test 709 | $ behave --include 帳號 710 | 功能: 用戶帳號 # features/帳號.feature:3 711 | 為了買賣商品 712 | 身為買家或賣家 713 | 我想要有一個電子商務網站帳號 714 | 場景: 用正確的帳號跟密碼登入 # features/帳號.feature:8 715 | 假設 < 帳號django與密碼django123已註冊 # features/steps/步驟.py:3 0.001s 716 | 當 < 我用django與密碼django123登入 # features/steps/步驟.py:7 0.001s 717 | 那麼 < 我得到登入結果:成功 # features/steps/步驟.py:14 0.000s 718 | 719 | 場景: 用不正確的帳號跟密碼登入 # features/帳號.feature:13 720 | 假設 < 帳號django與密碼django123已註冊 # features/steps/步驟.py:3 0.010s 721 | 當 < 我用django與密碼abcdef123登入 # features/steps/步驟.py:7 0.001s 722 | 那麼 < 我得到登入結果:失敗 # features/steps/步驟.py:14 0.000s 723 | 724 | 場景大綱: 帳號與密碼必須大於5個字元 -- @1.1 一些帳號與密碼 # features/帳號.feature:24 725 | 當 < 嘗試用帳號abc與密碼123456註冊 # features/steps/步驟.py:18 0.000s 726 | 那麼 < 我得到註冊結果:無效的帳號或密碼 # features/steps/步驟.py:25 0.000s 727 | 728 | 場景大綱: 帳號與密碼必須大於5個字元 -- @1.2 一些帳號與密碼 # features/帳號.feature:25 729 | 當 < 嘗試用帳號abcedf與密碼123註冊 # features/steps/步驟.py:18 0.000s 730 | 那麼 < 我得到註冊結果:無效的帳號或密碼 # features/steps/步驟.py:25 0.000s 731 | 732 | 場景大綱: 帳號與密碼必須大於5個字元 -- @1.3 一些帳號與密碼 # features/帳號.feature:26 733 | 當 < 嘗試用帳號abc與密碼123註冊 # features/steps/步驟.py:18 0.000s 734 | 那麼 < 我得到註冊結果:無效的帳號或密碼 # features/steps/步驟.py:25 0.000s 735 | 736 | 場景大綱: 帳號與密碼必須大於5個字元 -- @1.4 一些帳號與密碼 # features/帳號.feature:27 737 | 當 < 嘗試用帳號abcdef與密碼123456註冊 # features/steps/步驟.py:18 0.002s 738 | 那麼 < 我得到註冊結果:帳號建立 # features/steps/步驟.py:25 0.000s 739 | 740 | 1 feature passed, 0 failed, 0 skipped 741 | 6 scenarios passed, 0 failed, 0 skipped 742 | 14 steps passed, 0 failed, 0 skipped, 0 undefined 743 | Took 0m0.016s 744 | ``` 745 | 746 | > 這邊有個環境的設定問題:因為測試註冊功能會在資料庫插入一筆資料,數次實驗後會有多筆,如果每次測試前沒有清空資料庫,就會測試失敗... 每次測試,環境都要獨立、要獨立、要獨立,獨立很重要所以說三次!這部分先欠著,等使用 behave-django 再交代清楚。 747 | 748 | ## Using V Models for Testing 749 | 750 | ![V Model](Systems_Engineering_Process_II.png) 751 | 752 | (圖片來源 https://en.wikipedia.org/wiki/V-Model) 753 | 754 | - 軟體開發從定義需求開始,透過文字 PM 與 QC 共同定義 features 755 | - QC 將 features 轉成可執行的 steps 756 | - RD 透過 TDD 方式實作 functions,滿足 features 757 | - PM 與 QC 確認功能正常 758 | --------------------------------------------------------------------------------