小码奔腾

记录一些和自动化测试、CI有关的想法


  • Home

  • Archives

测试工程师的七项能力

Posted on 2018-08-21

继续读茹大师的课程,其中一课对现代测试工程师的能力建设做了一次画像或者说总结分析,我用MindMaster做了一个思维导图。

现代测试工程师的能力建设总结

怎么样更好地设计测试用例

Posted on 2018-07-05

“好的”测试用例所具备的特点

  • 整体完备性:好的测试用例是一个完备的整体,是有效测试用例组成的集合,能够完全覆盖测试需求
  • 等价类划分的准确性:每个等价类都能保证只要其中一个输入通过,其他输入也一定测试通过
  • 等价类集合的完备性:需要保证所有可能的边界值和边界条件都已经正确识别

三种最常用的测试用例设计方法

等价类划分方法

1
2
3
4
5
6
7
8
9
10
11
举例:学生信息系统的录入页面中“考试成绩”一项。

成绩的取值范围是0~100之间的整数,考试成绩及格的分数线是60.

有效等价类1:0-59之间的任意整数
有效等价类2:59-100之间的任意整数

无效等价类1:小于0的负数
无效等价类2:大于100的整数
无效等价类3:0-100之间的任何浮点数
无效等价类4:其他任意非数字字符

边界值分析方法

边界值分析是对等价类划分的补充,从工程实践经验中可以发现,大量的错误发生在输入输出的边界值上,所以需要对边界值进行重点测试。通常选取正好等于、刚刚大于、刚刚小于边界的值,作为测试数据。针对上面的学生信息系统的例子,选取的边界值数据应该包括:

-1
0
1
59
60
61
99
100
101

错误推测方法

错误推测方法是指基于对被测软件系统设计的理解、过去的经验和个人的自觉,推测出软件可能存在的缺陷,从而有针对性地设计测试用例的方法,这里强调的是对被测软件的需求理解以及设计实现的细节把握,还有个人的能力。在企业的具体实践中,通常会建立起常见缺陷知识库,在测试设计过程中,依据缺陷知识库作为checklist,来优化测试设计。同时,还需要组织和鼓励测试工程师之间的技术和技术交流,减少测试设计中可能存在盲点

某些测试用例可能更依赖于过去发生过的问题,和测试人员的个人能力

举例:
web界面的GUI功能测试,需要对浏览器的缓存与否进行分支考虑
web service的API测试,需要考虑被测试的API所依赖的第三方API出错的情况下,所应有的处理逻辑
单元测试,需要考虑被测函数的输入参数为空的情况下,所应有的处理逻辑

测试基础架构比较成熟的大中型企业中,通常还会依据缺陷知识库,自动整理生成数据驱动测试中的测试输入数据

以面向终端用户的GUI测试来讨论测试用例设计的流程、注意事项

最核心的测试点是验证软件对需求的满足程度,这就要求QA人员对被测软件的需求有深入的理解。而理解需求最好的办法则是QA人员在需求分析和设计阶段就开始介入,因为这个阶段是理解和掌握软件的原始业务需求的最好时机。

朴素的测试用例设计流程

首先搞清楚每一个业务需求说对应的多个软件功能需求点 >> 分析出每个软件功能需求点对应的多个测试需求点 >> 针对每个测试需求点设计测试用例

回到测试用例本身的设计

需要注意的关键点:

  • 从软件功能的需求触发,全面而细致地识别出测试需求,这会直接关系到测试用例的覆盖率
  • 对于识别出的每个测试需求点,要综合运用等价类划分和边界值分析,和错误推测方法来全面而灵活地设计测试用例

有关用例设计的其他经验总结

作为测试人员,只有深入理解被测软件的架构,才能设计出更加有目标性的测试用例集合,去发现系统边界和系统集成中的潜在缺陷。

不可用开发代码的实现为依据设计测试用例或自动化测试代码,应该依据原始需求来设计。

需要引入需求覆盖率和代码覆盖率来衡量测试执行的完备性,并以此为依据寻找遗漏的测试点。

更多的补充

测试用例本身也具有和被测试软件版本一一对应的版本属性,也即需要在软件需求变更等情况下,对测试用例进行更新和维护。同时测试用例的维护工作也是测试工作的重要组成部分,其花费的时间,也需要计入测试设计的整体考虑。

测试用例的语言描写,需要简洁而明确,在测试部门可以使用一些公有缩写约定来简化测试用例的书写,提高测试用例的开发速度。

有关用户登录场景的测试用例设计

Posted on 2018-07-05

订阅了一份软件测试相关的栏目,边看着也想就此整理一下从业这么多年来软件测试方面的相关知识与技术。下面是看其中一篇关于用户登录的测试用例设计的文章所做的笔记,自己做了些归纳整理,在实际工作中可以作为很好的参考,从不同角度来分析类似的问题,就可以获得比较清晰的测试用例设计思路了。

PS: MindMaster这个工具不错,可以自适应调整各个子分支之间的距离,大体上结构整理的挺美观的。

为用户登录设计测试用例的思维导图

在DigitalOcean云服务器上部署Django项目实践笔记

Posted on 2018-06-28

首先得有一份Django app项目代码,在本地调试模式下(python manage.py runserver)跑起来各种没问题。

配置云服务器

在DigitalOcean上申请Ubuntu 16.04LTS的Droplet,拿到公网IP和root密码,登陆后安装Django项目中用到的数据库软件,比如MySql:

1
2
3
4
5
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get install mysql-server
sudo apt isntall mysql-client
sudo apt install libmysqlclient-dev

执行命令sudo netstat -tap | grep mysql来检查MySql是否在运行,登录打开MySql提示符执行mysql> create database proj_db_name character set utf8;来预创建Django项目要用到的数据库,这里数据库名字为proj_db_name。

执行sudo apt-get -y install nginx安装NGINX,用来服务支持静态文件(css,js, images),还可以在代理服务器下运行Django app。

安装Supervisor,用来启动和管理Django应用程序服务。

1
2
3
sudo apt-get -y install supervisor
sudo systemctl enable supervisor
sudo systemctl start supervisor

安装Python 3.x,我实践中是安装的3.6版本,那就需要去下载Python 3.6源码做编译安装。

执行sudo apt-get -y install python-virtualenv安装Python Virtualenv,使用virtualenv已然是Python项目的最佳实践之一,它可以方便的建立独立python依赖空间,避免项目之间可能的依赖冲突问题。一般实践中是virtualenv venv在项目根目录下生成保存独立python环境与依赖的venv目录。

添加项目专用的用户,执行adduser joe,这里joe就是用户名,然后gpasswd -a joe sudo加入到sudo用户列表。

执行su - joe切换到joe账号,当前目录应该是/home/joe,再执行virtualenv -p python3 .,也即在当前目录生成一份独立python环境,python版本和系统命令python3指向的python版本一致,如果不加-p python3则就和默认命令python指向的python版本一致,再继续执行source bin/activate启用这个独立虚拟python环境。

1
2
3
4
joe@ubuntu-s-1vcpu-1gb-sfo2:~$ source bin/activate
(joe) joe@ubuntu-s-1vcpu-1gb-sfo2:~$ ls
bin include lib logs pip-selfcheck.json run
(joe) joe@ubuntu-s-1vcpu-1gb-sfo2:~$

设置Django项目

git clone项目代码到/home/joe,执行git clone https://github.com/python012/guest.git,继续执行pip install -r guest/requirements.txt安装所有依赖。

注意这里guest是Django项目(as Django app)名字。

修改Django项目中的./guest/guest/settings.py,以下是修改后的一个diff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
-DEBUG = True
+DEBUG = False # 在生产环境中必须关闭DEBUG模式

-ALLOWED_HOSTS = []
+ALLOWED_HOSTS = ['167.99.104.197',] # 云主机的公网IP


# Application definition
@@ -81,11 +81,11 @@ DATABASES = {
'ENGINE': 'django.db.backends.mysql',
- 'HOST': '---------',
+ 'HOST': '127.0.0.1',
'PORT': '3306',
- 'NAME': 'guest',
+ 'NAME': 'proj_db_name',
'USER': 'MySqlUsername',
- 'PASSWORD': '---------',
+ 'PASSWORD': 'MysqlPassword',
'OPTIONS': {
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
@@ -132,9 +132,10 @@ USE_TZ = False

STATIC_URL = '/static/'

+STATIC_ROOT = '/home/joe/www/static' # 静态文件目录

回到当前目录/home/joe,生成静态文件目录mkdir /home/joe/www/static,执行python manage.py migrate连接MySql初始化各个数据表,再执行python manage.py createsuperuser来生成管理员账户,最后执行python manage.py collectstatic。

设置Gunicorn、Supervisor、NGINX

执行pip install gunicorn安装Gunicorn(可能是发音G unicorn),这是一个WSGI容器(WSGI HTTP Server)。

执行vim bin/gunicorn_start,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/bash

NAME="guest"
DIR=/home/joe/guest
USER=joe
GROUP=joe
WORKERS=3
BIND=unix:/home/joe/run/gunicorn.sock
DJANGO_SETTINGS_MODULE=guest.settings
DJANGO_WSGI_MODULE=guest.wsgi
LOG_LEVEL=error

cd $DIR
source ../bin/activate

export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DIR:$PYTHONPATH

exec ../bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $WORKERS \
--user=$USER \
--group=$GROUP \
--bind=$BIND \
--log-level=$LOG_LEVEL \
--log-file=-

执行chmod u+x bin/gunicorn_start,给gunicorn_start文件添加执行权限。在用户目录/home/joe下生成run、logs两个目录。执行touch logs/gunicorn-error.log用来保存Django app错误日志。

执行sudo vim /etc/supervisor/conf.d/guest.conf生成一个Supervisor配置文件,文件内容如下:

1
2
3
4
5
6
7
[program:guest]
command=/home/joe/bin/gunicorn_start
user=joe
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/home/joe/logs/gunicorn-error.log

刷新Supervisor的配置文件信息,启动Django app。

1
2
sudo supervisorctl reread
sudo supervisorctl update

继续设置Nginx,执行sudo vim /etc/nginx/sites-available/guest生成Django app对应的配置文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
upstream app_server {
server unix:/home/joe/run/gunicorn.sock fail_timeout=0;
}

server {
# 如果设置为80则可以外网用户可以通过直接访问云主机的公网IP来打开Django app
listen 8089;

# add here the ip address of your server
# or a domain pointing to that ip (like example.com or www.example.com)
server_name 167.99.104.197;

keepalive_timeout 5;
client_max_body_size 4G;

access_log /home/joe/logs/nginx-access.log;
error_log /home/joe/logs/nginx-error.log;

location /static/ {
alias /home/joe/www/static/;
}

# checks for static file, if not found proxy to app
location / {
try_files $uri @proxy_to_app;
}

location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app_server;
}
}

执行sudo ln -s /etc/nginx/sites-available/guest /etc/nginx/sites-enabled/guest创建符号链接,执行sudo rm /etc/nginx/sites-enabled/default删除NGINX默认的网站配置,最后执行sudo service nginx restart来重启nginx,这时候应该能够通过云服务器的公网IP访问Django app了,注意还需要加上配置的端口号8089,如果设置为80则可直接访问IP了。如果后期Django项目代码有更改,可以执行sudo supervisorctl restart guest来重启Djang app以应用最新项目代码。

为Django项目应用Django REST framework实现REST风格的Web API

Posted on 2018-06-26

Django REST framework实现的REST风格的Web API,同时又可以用浏览器进行查看,一个快速的例子就是https://restframework.herokuapp.com/users/,Django官方提供的一个范例,简洁明了,Django REST framework这套框架可以帮助Django项目快速实现REST风格的API,十分Pythonic。

假如基于Django已经实现了一个简单的Web项目(项目中实现了一个app,名为api,./project_name/api/models.py中已经定义了api中用到的model数据表class Person(models.Model))。

STEP 1 - 首先去./project_name/project_name/settings.py,在INSTALLED_APPS中添加rest_framework:

1
2
3
4
5
6
7
8
9
10
11
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api',
'bootstrap3',
'rest_framework',
]

STEP 2 - 创建./project_name/api/serializers.py,基本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.contrib.auth.models import User, Group
from rest_framework import serializers
from api.models import Person

class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')

class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ('url', 'name')

class PersonSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Person
fields = ('name', 'address', 'status')

STEP 3 - 创建./project_name/api/view_interface_rest.py,基本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.contrib.auth.models import Group, User
from rest_framework import viewsets

from api.models import Person
from api.serializers import GroupSerializer, UserSerializer, PersonSerializer

class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to viewed or edited.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer

class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to viewed or edited.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer

class PersonViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows events to viewed or edited.
"""
queryset = Person.objects.all()
serializer_class = PersonSerializer

STEP 4 - 打开./project_name/perject_name/urls.py,添加REST API的路由信息,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.conf.urls import url, include
from django.contrib import admin
from api import views, views_interface_rest

from rest_framework import routers

router = routers.DefaultRouter()
router.register(r'users', views_interface_rest.UserViewSet)
router.register(r'groups', views_interface_rest.GroupViewSet)
router.register(r'persons', views_interface_rest.EventViewSet)

urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', views.index),
url(r'^index/$', views.index),
url(r'^login_action/$', views.login_action),
url(r'^accounts/login/$', views.index),
url(r'^logout/$', views.logout),

url(r'^rest/', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

THE END - 此时代码工作应该是完成了,登陆访问http://127.0.0.1:8000/rest/就可以打开api root的页面,使用PostMan或者HTTPie等工具向http://127.0.0.1:8000/rest/发送GET请求,同时带上basic auth的用户名密码,就能拿到类似如下的response。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rx:guest reed$ http -a admin:password http://127.0.0.1:8000/rest/
HTTP/1.1 200 OK
Allow: GET, HEAD, OPTIONS
Content-Length: 183
Content-Type: application/json
Date: Wed, 27 Jun 2018 01:17:15 GMT
Server: WSGIServer/0.2 CPython/3.6.5
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
"persons": "http://127.0.0.1:8000/rest/persons/",
"groups": "http://127.0.0.1:8000/rest/groups/",
"users": "http://127.0.0.1:8000/rest/users/",
}

Django development实践笔记

Posted on 2018-06-02

买到一本有趣的关于Django开发和接口测试的书,写的很棒,这里随手记录以下Django开发的要点。

项目的启动

  1. 推荐用Python 3.x,pip install django==1.10.3.
  2. 安装成功后,如果django-admin程序在PATH目录的话,运行django-admin可以看到命令列表。
  3. 执行django-admin guest来创建一个叫guest的项目。
  4. 进入guest目录,执行python3 manage.py来查看manage所提供的命令列表。
  5. 执行python3 manage.py startapp appname,创建一个app,名为appname。

Django中的app

Django框架下的Web开发有很多有意思的概念,比如这个app概念,我理解就是完成一个比较独立的功能的模块。

创建了名为appname的app后,会有appname目录生成,其中会有以下文件或者目录:

  • migrations/ 用来记录modles中的数据变更
  • admin.py 映射modles中的数据到Django自带的admin后台
  • apps.py 用于app的配置
  • modles.py Django模型文件,创建app的数据表模型,对应数据库的相关操作
  • tests.py 创建测试用例
  • views.py Django的视图文件,控制向前段页面显示的内容
  • templates/ 用来放HTML模版文件

app创建之后,还需要去项目目录下的settings.py,把app名字添加到INSTALLED_APPS中。

一次具体的网站页面访问到底是怎样一个过程呢?

假设有一个登陆后可以看到图片列表的网站,现在开始打开浏览器,输入http://localhost/login/敲回车开始访问,这里我尝试用在Django框架里发生的一系列事件来解释下这个过程。

  1. Django框架下的服务器程序会接受到一个针对/login/的HTTP GET request。
  2. Django会依据项目目录下的urls.py中的urlpatterns(如下)来查找/login/所绑定的视图函数。
1
2
3
4
5
6
7
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^$', views.login),
url(r'^login/$', views.login),
url(r'^login_action/$', views.login_action),
url(r'^image_list/$', views.image_list),
]
  1. 查找后发现/login/绑定的是函数views.login(request)(存在于/appname/views.py),然后会把这个http request对象作为参数传给views.login(request)。
  2. 函数views.login(request)的内容如下,它会返回index.html文件,这是事先写好放在/appname/templates目录里的。没有学习Web开发之前,可能会认为URL名字总是有相应的HTML文件,实际在Django Web开发中却不是这样的。
1
2
def login(request):
return render(request, "index.html")
  1. index.html中的内容会呈现在浏览器中,此刻浏览器的地址栏还是http://localhost/login/。
  2. 这里需要说下index.html,源文件中多半有下面这样的代码.
1
2
3
4
5
6
7
8
9
<form class="center" action="/login_action/" method="POST">
<input type="text" name="username" placeholder="username">
<br>
<input type="password" name="password" placeholder="password">
<br> {{ error_message }}
<br>
<button id="btn" type="submit">登录</button>

</form>

填写用户名和密码,并提交的页面元素是一个form,填好后点击按钮,form里定义的action="/login_action/" method="POST",决定了填好的这些username和password信息,是向http://localhost/login_action/提交(POST方法)。这里的error_message和csrf_token,一个是预备载入错误信息的地方,一个是django中的防止跨站请求伪造的token,先不展开了。

  1. 然后就重复之前的步骤,服务器会从urls.py中查找路径/login_action/所绑定的视图函数,这里是views.login_action,然后把http request对象传过去作为参数。
  2. 来看下login_action()函数的细节
1
2
3
4
5
6
7
8
9
10
11
12
13
def login_action(request):
if request.method == 'POST':
username = request.POST.get('username', '')
password = request.POST.get('password', '')
user = auth.authenticate(username=username, password=password)

if user is not None:
auth.login(request, user)
request.session['user'] = username
response = HttpResponseRedirect('/image_list/')
return response
else:
return render(request, 'index.html', {'error_message': 'username or password is not correct!'})

如果login_action()函数收到的http request是一个POST,就会从rquest中解析出username/password信息,然后用Django提供的通用认证方法进行检查,如果一切ok,user变量就不为空,然后把request session中的user置为username的值(刚刚由request中解析出来),再把request重定向到/image_list/。

  1. 再一次重复上面的步骤,服务器会从urls.py中查找路径/image_list/所绑定的视图函数,此处是views.image_list,然后把http request对象传过去作为参数。
  2. 看一下views.image_list(request)函数:
1
2
3
4
5
@login_required
def image_list(request):
username = request.session.get('user', '')
image_list = Image.objects.all()
return render(request, "image_list.html", {"user": username, "images": image_list})

views.image_list收到http request后,先从session中获得user变量的值,也即最开始用户登陆的时候输入的用户名,再从系统中的Image modle(定义在项目根目录的models.py里)获取到所有的Image对象列表(这里有很多Django的辅助实施),然后把user变量的值,和Image对象列表组成一个字典(或可称为json串?)传给/appname/templates里image_list.html,Django会使用字典作为参数对html文件进行加工,比如把user变量的值放在网页右上角显示为欢迎 User,然后依据Image对象列表找到所有Image的地址,生成一些列li标签,每个li带一个image,最后看起来像一个相册。

  1. 最后浏览器的地址栏会停留在http://localhost/image_list/,浏览器的主页面会显示经过Django处理过的,以image_list.html为模板的网页。

最后一点题外话,发现hexo似乎有个bug,blog的md文本中里不能出现用%{包围的csrf_token,否则会hexo s失败,报错Template render error: unknown block tag: csrf_token,所以文中引用的第一段HTML代码中button tag下原本是有csrf_token的。

12…6
Reed Xia

Reed Xia

记录一些和自动化测试、CI有关的想法

33 posts
32 tags
GitHub E-Mail
© 2018 Reed Xia
Hosted by GitHub Pages