
专注于软件测试技术的分享与推广
有时候我们会碰到一些元素不可见,这个时候selenium就无法对这些元素进行操作了。例如,下面的情况: Python 页面主要通过“display:none”来控制整个下拉框不可见。这个时候如果直接操作这个下拉框,就会提示: from selenium import webdriver from selenium.webdriver.support.select import Select import os,time driver = webdriver.Chrome() file_path = 'file:///' + os.path.abspath('test.html') driver.get(file_path) sel = driver.find_element_by_tag_name('select') Select(sel).select_by_value('opel') time.sleep(2) driver.quit() exceptions.ElementNotVisibleException: Message: element not visible: Element is not currently visible and may not be manipulated 我们需要通过javaScript修改display的值。 …… js = 'document.querySelectorAll("select")[0].style.display="block";' driver.execute_script(js) sel = driver.find_element_by_tag_name('select') Select(sel).select_by_value('opel') …… document.querySelectorAll("select")[0].style.display="block"; document.querySelectorAll("select") 选择所有的select。 [0] 指定这一组标签里的第几个。 style.display="block"; 修改样式的display="block" ,表示可见。 执行完这句js代码后,就可以正常操作下拉框了。 Java 以下为java中的操作 package com.jase.base; import java.io.File; import org.openqa.selenium.WebDriver; import org.openqa.selenium.By.ById; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.support.ui.Select; import org.openqa.selenium.JavascriptExecutor; public class SelectTest { public static void main(String[] args){ WebDriver driver = new ChromeDriver(); File file = new File("C:/Users/fnngj/Desktop/test.html"); String filePath = file.getAbsolutePath(); driver.get(filePath); String js = "document.querySelectorAll('select')[0].style.display='block';"; ((JavascriptExecutor)driver).executeScript(js); Select sel = new Select(driver.findElement(ById.xpath("//select"))); sel.selectByValue("opel"); } }
有时候我们会碰到<select></select>标签的下拉框。直接点击下拉框中的选项不一定可行。Selenium专门提供了Select类来处理下拉框。 <select id="status" class="form-control valid" onchange="" name="status"> <option value=""></option> <option value="0">未审核</option> <option value="1">初审通过</option> <option value="2">复审通过</option> <option value="3">审核不通过</option> </select> Python 先以python为例,查看Selenium代码select.py文件的实现: ...\selenium\webdriver\support\select.py class Select: def __init__(self, webelement): """ Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not, then an UnexpectedTagNameException is thrown. :Args: - webelement - element SELECT element to wrap Example: from selenium.webdriver.support.ui import Select \n Select(driver.find_element_by_tag_name("select")).select_by_index(2) """ if webelement.tag_name.lower() != "select": raise UnexpectedTagNameException( "Select only works on <select> elements, not on <%s>" % webelement.tag_name) self._el = webelement multi = self._el.get_attribute("multiple") self.is_multiple = multi and multi != "false" 查看Select类的实现需要一个元素的定位。并且Example中给了例句。 Select(driver.find_element_by_tag_name("select")).select_by_index(2) def select_by_index(self, index): """Select the option at the given index. This is done by examing the "index" attribute of an element, and not merely by counting. :Args: - index - The option at this index will be selected """ match = str(index) matched = False for opt in self.options: if opt.get_attribute("index") == match: self._setSelected(opt) if not self.is_multiple: return matched = True if not matched: raise NoSuchElementException("Could not locate element with index %d" % index) 继续查看select_by_index() 方法的使用并符合上面的给出的下拉框的要求,因为它要求下拉框的选项必须要有index属性,例如index=”1”。 def select_by_value(self, value): """Select all options that have a value matching the argument. That is, when given "foo" this would select an option like: <option value="foo">Bar</option> :Args: - value - The value to match against """ css = "option[value =%s]" % self._escapeString(value) opts = self._el.find_elements(By.CSS_SELECTOR, css) matched = False for opt in opts: self._setSelected(opt) if not self.is_multiple: return matched = True if not matched: raise NoSuchElementException("Cannot locate option with value: %s" % value) 继续查看select_by_value() 方法符合我们的需求,它用于选取<option>标签的value值。最终,可以通过下面有实现选择下拉框的选项。 from selenium.webdriver.support.select import Select …… sel = driver.find_element_by_xpath("//select[@id='status']") Select(sel).select_by_value('0') #未审核 Select(sel).select_by_value('1') #初审通过 Select(sel).select_by_value('2') #复审通过 Select(sel).select_by_value('3') #审核不通过 Java 当然,在java中的用法也类似,唯一不区别在语法层面有。 package com.jase.base; import org.openqa.selenium.WebDriver; import org.openqa.selenium.By.ById; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.support.ui.Select; public class SelectTest { public static void main(String[] args){ WebDriver driver = new ChromeDriver(); driver.get("http://www.you_url.com"); // …… Select sel = new Select(driver.findElement(ById.xpath("//select[@id='status']"))); sel.selectByValue("0"); //未审核 sel.selectByValue("1"); //初审通过 sel.selectByValue("2"); //复审通过 sel.selectByValue("3"); //审核不通过 } }
虽然,很久不用关于Robot Framework框架了,但我这里应该是除了@齐涛-道长之外分享Robot Framework 相关资料比较多的地方了。所以,常常被问到一些关于该框架的问题。 虽然,我一直坚信该框架的无比强大和简单好用,并且,会越发展越来好。但是,对于习惯了直接写代码的自由,很难在回头用它,但这并不妨碍我对该框架的关注! 本篇介绍一下如何使用Robot Framework的Jybot 模式。 安装环境: ================ Python : robot framework是基于python开发的。(如果不使用pybot,可以不装) JDK : 为了使用Jybot,(必装)。 Jython :Jython基于jvm虚拟机开发的Python语法。通过它可以调用Java程序或Java的标准库。(必装) Robot framework :要想使用该框架(必装)。 Robot framework-ride :可以看作Robot Framework框架的标准编辑器,如果不想用,可以不装。 wxPython :如果使用ride 的话,不用装。 ================ 安装步骤参考: http://www.cnblogs.com/fnng/p/3871712.html http://www.cnblogs.com/fnng/p/4960697.html 注意:为了使用Jybot ,Robot framework 除了需要安装到Python下面之外,还需要再安装在Jython下面。 首先证明,Jython安装成功。 然后,下载robot framework包,解压,进入目录通过:“jython setup.py install ”命令安装。 安装好后,输入“jybot”命令检验是否成功。 接下来做一个简单的练习,在E:/rf/目录下创建test.robot文件,内容过于简单,我就直接上编辑器截图了。 以免图片失效,还是贴一下用例吧! *** Test Cases *** case log jybot run test case 再接下来通过“jybot”运行测试用例文件(> jybot test.robot): 查看log.html结果:
Django的部署可以有很多方式,采用nginx+uwsgi的方式是其中比较常见的一种方式。 在这种方式中,我们的通常做法是,将nginx作为服务器最前端,它将接收WEB的所有请求,统一管理请求。nginx把所有静态请求自己来处理(这是NGINX的强项)。然后,NGINX将所有非静态请求通过uwsgi传递给Django,由Django来进行处理,从而完成一次WEB请求。 可见,uwsgi的作用就类似一个桥接器。起到桥梁的作用。 Linux的强项是用来做服务器,所以,下面的整个部署过程我们选择在Ubuntu下完成。 一、安装Nginx Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好。 Nginx同样为当前非常流行的web服务器。利用其部署Django,我们在此也做简单的介绍。 Nginx官网:http://nginx.org/ 打开ubuntu控制台(ctrl+alt+t)利用Ubuntu的仓库安装。 fnngj@ubuntu:~$ sudo apt-get install nginx #安装 启动Nginx: fnngj@ubuntu:~$ /etc/init.d/nginx start #启动 fnngj@ubuntu:~$ /etc/init.d/nginx stop #关闭 fnngj@ubuntu:~$ /etc/init.d/nginx restart #重启 修改Nginx默认端口号,打开/etc/nginx/nginx.conf 文件,修改端口号。 server { listen 8088; # 修改端口号 server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root html; index index.html index.htm; } 大概在文件36行的位置,将默认的80端口号改成其它端口号,如 8088。因为默认的80端口号很容易被其它应用程序占用。 然后,通过上面命令重启nginx。访问:http://127.0.0.1:8088/ 如果出现如上图,说明Nginx启动成功。 二、安装uwsgi 通过pip安装uwsgi。 root@ubuntu:/etc# python3 -m pip install uwsgi 测试uwsgi,创建test.py文件: def application(env, start_response): start_response('200 OK', [('Content-Type','text/html')]) return [b"Hello World"] 通过uwsgi运行该文件。 fnngj@ubuntu:~/pydj$ uwsgi --http :8001 --wsgi-file test.py 接下来配置Django与uwsgi连接。此处,假定的我的django项目位置为:/home/fnngj/pydj/myweb fnngj@ubuntu:~/pydj$ uwsgi --http :8001 --chdir /home/fnngj/pydj/myweb/ --wsgi-file myweb/wsgi.py --master --processes 4 --threads 2 --stats 127.0.0.1:9191 常用选项: http : 协议类型和端口号 processes : 开启的进程数量 workers : 开启的进程数量,等同于processes(官网的说法是spawn the specified number ofworkers / processes) chdir : 指定运行目录(chdir to specified directory before apps loading) wsgi-file : 载入wsgi-file(load .wsgi file) stats : 在指定的地址上,开启状态服务(enable the stats server on the specified address) threads : 运行线程。由于GIL的存在,我觉得这个真心没啥用。(run each worker in prethreaded mode with the specified number of threads) master : 允许主进程存在(enable master process) daemonize : 使进程在后台运行,并将日志打到指定的日志文件或者udp服务器(daemonize uWSGI)。实际上最常用的,还是把运行记录输出到一个本地文件上。 pidfile : 指定pid文件的位置,记录主进程的pid号。 vacuum : 当服务器退出的时候自动清理环境,删除unix socket文件和pid文件(try to remove all of the generated file/sockets) 三、Nginx+uwsgi+Django 接下来,我们要将三者结合起来。首先罗列一下项目的所需要的文件: myweb/ ├── manage.py ├── myweb/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── myweb_uwsgi.ini 在我们通过Django创建myweb项目时,在子目录myweb下已经帮我们生成的 wsgi.py文件。所以,我们只需要再创建myweb_uwsgi.ini配置文件即可,当然,uwsgi支持多种类型的配置文件,如xml,ini等。此处,使用ini类型的配置。 # myweb_uwsgi.ini file [uwsgi] # Django-related settings socket = :8000 # the base directory (full path) chdir = /home/fnngj/pydj/myweb # Django s wsgi file module = myweb.wsgi # process-related settings # master master = true # maximum number of worker processes processes = 4 # ... with appropriate permissions - may be needed # chmod-socket = 664 # clear environment on exit vacuum = true 这个配置,其实就相当于在上一小节中通过wsgi命令,后面跟一堆参数的方式,给文件化了。 socket 指定项目执行的端口号。 chdir 指定项目的目录。 module myweb.wsgi ,可以这么来理解,对于myweb_uwsgi.ini文件来说,与它的平级的有一个myweb目录,这个目录下有一个wsgi.py文件。 其它几个参数,可以参考上一小节中参数的介绍。 接下来,切换到myweb项目目录下,通过uwsgi命令读取myweb_uwsgi.ini文件启动项目。 fnngj@ubuntu:~$ cd /home/fnngj/pydj/myweb/ fnngj@ubuntu:~/pydj/myweb$ uwsgi --ini myweb_uwsgi.ini [uWSGI] getting INI configuration from myweb_uwsgi.ini *** Starting uWSGI 2.0.12 (32bit) on [Sat Mar 12 13:05:06 2016] *** compiled with version: 4.8.4 on 26 January 2016 06:14:41 os: Linux-3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:18:00 UTC 2015 nodename: ubuntu machine: i686 clock source: unix detected number of CPU cores: 2 current working directory: /home/fnngj/pydj/myweb detected binary path: /usr/local/bin/uwsgi !!! no internal routing support, rebuild with pcre support !!! chdir() to /home/fnngj/pydj/myweb your processes number limit is 15962 your memory page size is 4096 bytes detected max file descriptor number: 1024 lock engine: pthread robust mutexes thunder lock: disabled (you can enable it with --thunder-lock) uwsgi socket 0 bound to TCP address :8000 fd 3 Python version: 3.4.3 (default, Oct 14 2015, 20:37:06) [GCC 4.8.4] *** Python threads support is disabled. You can enable it with --enable-threads *** Python main interpreter initialized at 0x8b52dc0 your server socket listen backlog is limited to 100 connections your mercy for graceful operations on workers is 60 seconds mapped 319920 bytes (312 KB) for 4 cores *** Operational MODE: preforking *** WSGI app 0 (mountpoint='') ready in 1 seconds on interpreter 0x8b52dc0 pid: 7158 (default app) *** uWSGI is running in multiple interpreter mode *** spawned uWSGI master process (pid: 7158) spawned uWSGI worker 1 (pid: 7160, cores: 1) spawned uWSGI worker 2 (pid: 7161, cores: 1) spawned uWSGI worker 3 (pid: 7162, cores: 1) spawned uWSGI worker 4 (pid: 7163, cores: 1) 注意查看uwsgi的启动信息,如果有错,就要检查配置文件的参数是否设置有误。 再接下来要做的就是修改nginx.conf配置文件。打开/etc/nginx/nginx.conf文件,添加如下内容。 …… server { listen 8099; server_name 127.0.0.1 charset UTF-8; access_log /var/log/nginx/myweb_access.log; error_log /var/log/nginx/myweb_error.log; client_max_body_size 75M; location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; uwsgi_read_timeout 2; } location /static { expires 30d; autoindex on; add_header Cache-Control private; alias /home/fnngj/pydj/myweb/static/; } } …… listen 指定的是nginx代理uwsgi对外的端口号。 server_name 网上大多资料都是设置的一个网址(例,www.example.com),我这里如果设置成网址无法访问,所以,指定的到了本机默认ip。 在进行配置的时候,我有个问题一直想不通。nginx到底是如何uwsgi产生关联。现在看来大概最主要的就是这两行配置。 include uwsgi_params; uwsgi_pass 127.0.0.1:8000; include 必须指定为uwsgi_params;而uwsgi_pass指的本机IP的端口与myweb_uwsgi.ini配置文件中的必须一直。 现在重新启动nginx,翻看上面重启动nginx的命令。然后,访问:http://127.0.0.1:8099/ 通过这个IP和端口号的指向,请求应该是先到nginx的。如果你在页面上执行一些请求,就会看到,这些请求最终会转到uwsgi来处理。 ============= ps: 这个过程本应不算复杂,之前花两天时间没搞定,索性放到了一边,这次又花了两天时间才算搞定。网上搜到的文章比较乱,有些太简单的看不懂,有些又太啰嗦的不知道核心的几步是什么,希望本文能帮到你。
在大多面向对象的编程语言中都提供了Interface(接口)的概念。如果你事先学过这个概念,那么在谈到“接口测试”时,会不会想起这个概念来!?本篇文章简单介绍一下面向对象编程语言中的Interface。 Java中的Interface 在Java中定义接口使用interface关键字来声明,可以看做是一种特殊的抽象类,可以指定一个类必须做什么,而不是规定它如何去做。 为什么使用接口? 大型项目开发中,可能需要从继承链的中间插入一个类,让它的子类具备某些功能而不影响它们的父类。例如 A -> B -> C -> D -> E,A 是祖先类,如果需要为C、D、E类添加某些通用的功能,最简单的方法是让C类再继承另外一个类。但是问题来了,Java 是一种单继承的语言,不能再让C继承另外一个父类了,只到移动到继承链的最顶端,让A再继承一个父类。这样一来,对C、D、E类的修改,影响到了整个继承链,不具备可插入性的设计。 接口是可插入性的保证。在一个继承链中的任何一个类都可以实现一个接口,这个接口会影响到此类的所有子类,但不会影响到此类的任何父类。此类将不得不实现这个接口所规定的方法,而子类可以从此类自动继承这些方法,这时候,这些子类具有了可插入性。 我们关心的不是哪一个具体的类,而是这个类是否实现了我们需要的接口。 接口提供了关联以及方法调用上的可插入性,软件系统的规模越大,生命周期越长,接口使得软件系统的灵活性和可扩展性,可插入性方面得到保证。 接口在面向对象的 Java 程序设计中占有举足轻重的地位。事实上在设计阶段最重要的任务之一就是设计出各部分的接口,然后通过接口的组合,形成程序的基本框架结构。 所以简单总结其用途为:实现类的多继承,以解决Java只能单继承,不支持多继承的问题。 下面通过例子介绍Java中接口的使用。 定义接口(IAnimal.java): package mypor.interfaces.demo; public interface IAnimal { public String Behavior(); //行为方法,描述各种动物的特性 } 实现接口一(Dog.java): package mypor.interfaces.demo; import mypor.interfaces.demo.IAnimal; //类: 狗 public class Dog implements IAnimal{ public String Behavior() { String ActiveTime = "我晚上睡觉,白天活动"; return ActiveTime; } } 实现接口二(Cat.java): package mypor.interfaces.demo; import mypor.interfaces.demo.IAnimal; //类:猫 public class Cat implements IAnimal{ public String Behavior() { String ActiveTime = "我白天睡觉,晚上捉老鼠。"; return ActiveTime; } } 测试接口的实现: package mypor.interfaces.demo; import mypor.interfaces.demo.Dog; import mypor.interfaces.demo.Cat; public class Test { public static void main(String[] args) { //调用dog和cat的行为 Dog d = new Dog(); Cat c = new Cat(); System.out.println(d.Behavior()); System.out.println(c.Behavior()); } } 注意,这里的测试,并不是测试的接口,因为接口本身只是简单的定义,没什么可测试的,这里真正所测试的是继承接口的类,或者叫已经实例化的对象。 Python中的Zope.interface 如果你和我一样更熟悉Python,那么是否想知道,Python中是否也有接口(Interface)的概念,Python本身并不提供提口的创建和使用,但是我们可以通过第三方扩展库来使用接口,那就是Zope.interface。 下载地址:https://pypi.python.org/pypi/zope.interface 来看个普通的例子: class Host(object): def goodmorning(self, name): """Say good morning to guests""" return "Good morning, %s!" % name if __name__ == '__main__': h = Host() hi = h.goodmorning('zhangsan') print(hi) 下面在这个例子的基础中使用接口: from zope.interface import Interface from zope.interface import implements # 定义接口 class IHost(Interface): def goodmorning(self,guest): """Say good morning to guest""" class Host(object): implements(IHost) # 实现接口 def goodmorning(self, guest): """Say good morning to guests""" return "Good morning, %s!" % guest if __name__ == '__main__': h = Host() hi = h.goodmorning('zhangsan') print(hi) 通过看本篇文章的例子,一定觉得接口是个特无聊的概念,我也有这种感觉。哈哈~!当真的碰到具体的用了接口会使结构更优雅的项目时,才会体会到它的意义。
最近一直在学习和整理web开发与接口测试的相关资料。接口测试本身毫无任何难度,甚至有很多工具和类库来帮助我们进行接口测试。大多测试人员很难深入了解web接口测试的原因是对web开发不太了解,当你越了解开发就会越看得清接口是什么。当然,web开发是比较麻烦,我们很难一下子掌握。 注:不过本文并不是一个零基础的文章,需要你对 Django web开发,requests接口库,unittest单元测试框架,三者有一定的了解。 Django快速开发之投票系统 之前分享过一篇Django开发投票系统的例子。今天在这个例子上做一些延伸,来讲讲web接口的开发与测试。 开发投票系统接口 虽然投票系统的的功能已经开发完成,但我们并没有开发专门的接口,在当前的投票系统中,在我们调用一个get或post请求时,系统会返回整个页面,并且把测试连同页面一起返回。 例如,当我们要调用所有问题的接口时(test_get.py) import requests base_url = 'http://127.0.0.1:8000/polls' r = requests.get(base_url) code = r.status_code text = r.text print(code) print(text) 得到如下结果: 200 <html lang="zh-CN"> <head> <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Polls System</a> </div> </div> </nav> <br><br> <div class="well"> <h3>Question List:</h3> <ul> <li><a href="/polls/1/">十一国庆七天假期做什么?</a></li> <li><a href="/polls/2/">你最想学的自动化工具是什么?</a></li> </ul> </div> <footer> <p>&copy; Company 2016 & chongshi</p> </footer> </body> </html> 而特有的接口应该返回的是数据,而不是整个页;而数据一般格式为Json格式。所以,需要对试图层(.../polls/views.py)进行改造,使其只提供接口,并单纯的返回数据。 from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from .models import Question, Choice from django.http import HttpResponse import json # Create your views here. # 查看所有问题 def index(request): latest_question_list = Question.objects.all() dicts = {} if latest_question_list: for question in latest_question_list: dicts[question.id] = question.question_text j = json.dumps(dicts) return HttpResponse(j) else: return HttpResponse("question list null") # 查看单个问题选项 def detail(request, question_id): choices = Choice.objects.filter(question_id=question_id) dicts = {} print(question_id) if question_id: for choice in choices: dicts[choice.id] = choice.choice_text j = json.dumps(dicts) return HttpResponse(j) ..... 为了节省时间,暂时先对查看所有问题、单个问题的所有选项两个功能进行接口改造,当然这里的改造也不够完整和健壮。例如单个问题的所有选项的接口,接收的参数question_id 如果为空,应该提示,参数错误。如果查询不到相关问题,应该提示,查询结果为空,如果传的类型不为数字,应该提示,类型错误。这些都是一个健壮的接口应有的处理逻辑。 再次执行test_get.py文件。 200 {"1": "\u5341\u4e00\u56fd\u5e86\u4e03\u5929\u5047\u671f\u505a\u4ec0\u4e48\uff1f", "2": "\u4f60\u6700\u60f3\u5b66\u7684\u81ea\u52a8\u5316\u5de5\u5177\u662f\u4ec0\u4e48\uff1f"} 这一次得到的就是json类型的数据了。不过,返回值对中文进行了unicode的编码。这里提供个小技巧,将其转换成中文。 打开Firefox浏览器的Firebug工具,切换到“控制台”标签。 编写接口文档 编写接口文档也是非常重要的一个环节,因为我们编写的接口是需要给别人调用的,那么别人如何知道我们的接口是用get还是post调用呢?参数都有哪些?当然需要参考接口文档了。 1、获取所有问题 url http://127.0.0.1:8000/polls 请求类型 get 需要参数 无 返回格式 json 返回结果 {"1": "十一国庆七天假期做什么?", "2": "你最想学的自动化工具是什么?" } 错误类型 暂无(接口代码需要补充逻辑) 2、获取单个问题的所有选项 url http://127.0.0.1:8000/polls/ 请求类型 get 需要参数 question_id 返回格式 json 返回结果 {"1": "旅行", "2":"看电影" , "3":"看书" } 错误类型 暂无(接口代码需要补充逻辑) …… 好啦!接口文档的大体结构就是上面的样子。有了这个份文档,我们接下来就很容易知道如何调用这些接口做测试了。 系统接口测试 对于编写接口测试来说,我们会涉及到两个技术。前面也都有过简单介绍,unittest单元测试框架和request库。 import unittest import requests class PollsTest(unittest.TestCase): def setUp(self): self.base_url = 'http://127.0.0.1:8000/polls' def tearDown(self): pass def test_get_poll_index(self): '''测试投票系统首页''' r = requests.get(self.base_url) code = r.status_code text = r.text self.assertEqual(code, 200) def test_get_poll_question(self): '''获得问题1的所有选项''' r = requests.get(self.base_url+'/1/') code = r.status_code text = r.text self.assertEqual(code, 200) self.assertIn("3",text) if __name__ == '__main__': unittest.main() 接口用例测试本身的编写是简单的,我们只用调用接口,传递不同的参数。从而验证返回值是否符合预期即可。
关于 PageFactory 的概念主要是Java中内置了PageFactory类。 import org.openqa.selenium.support.PageFactory; …… 例子,http://libin0019.iteye.com/blog/1260090 Python(Selenium)中没有这个类。 PageFactory 的概念和Page Object应该类似,属于一种设计模式。所以并不局限于语言及场景。于是,好奇,既然Java有,那Python也应该有类似的玩法。还真让我给找到了类似的实现。 原文在此:https://jeremykao.wordpress.com/2015/06/10/pagefactory-pattern-in-python/ 于是,就借助谷歌翻译加代码运行,弄懂了这哥们想要利用PageFactory 模式实现个什么东西,为了便于你的理解,我这里搬运过来给下结论。 selenium in python中的元素定位是这样的: find_element_by_id("kw") find_element_by_xpath("//*[@id='kw']") 或者是这样的: from selenium.webdriver.common.by import By find_element(By.ID,"kw") find_element(By.XPATH,"//*[@id='kw']") 通过PageFactory 模式的实现可以把元素定位变成这样的: from pageobject_support import callable_find_by as find_by find_by(id_="kw") find_by(xpath="//*[@id='kw']") 别看小小的改动,它其实使代码更容易的阅读和理解。 核心实现就是pageobject_support.py文件: __all__ = ['cacheable', 'callable_find_by', 'property_find_by'] def cacheable_decorator(lookup): def func(self): if not hasattr(self, '_elements_cache'): self._elements_cache = {} # {callable_id: element(s)} cache = self._elements_cache key = id(lookup) if key not in cache: cache[key] = lookup(self) return cache[key] return func cacheable = cacheable_decorator _strategy_kwargs = ['id_', 'xpath', 'link_text', 'partial_link_text', 'name', 'tag_name', 'class_name', 'css_selector'] def _callable_find_by(how, using, multiple, cacheable, context, driver_attr, **kwargs): def func(self): # context - driver or a certain element if context: ctx = context() if callable(context) else context.__get__(self) # or property else: ctx = getattr(self, driver_attr) # 'how' AND 'using' take precedence over keyword arguments if how and using: lookup = ctx.find_elements if multiple else ctx.find_element return lookup(how, using) if len(kwargs) != 1 or kwargs.keys()[0] not in _strategy_kwargs : raise ValueError( "If 'how' AND 'using' are not specified, one and only one of the following " "valid keyword arguments should be provided: %s." % _strategy_kwargs) key = kwargs.keys()[0]; value = kwargs[key] suffix = key[:-1] if key.endswith('_') else key # find_element(s)_by_xxx prefix = 'find_elements_by' if multiple else 'find_element_by' lookup = getattr(ctx, '%s_%s' % (prefix, suffix)) return lookup(value) return cacheable_decorator(func) if cacheable else func def callable_find_by(how=None, using=None, multiple=False, cacheable=False, context=None, driver_attr='_driver', **kwargs): return _callable_find_by(how, using, multiple, cacheable, context, driver_attr, **kwargs) def property_find_by(how=None, using=None, multiple=False, cacheable=False, context=None, driver_attr='_driver', **kwargs): return property(_callable_find_by(how, using, multiple, cacheable, context, driver_attr, **kwargs)) 然后,我再帖一下具体的例子: from pageobject_support import callable_find_by as find_by from selenium import webdriver class BaiduSearchPage(object): def __init__(self, driver): self._driver = driver search_box = find_by(id_="kw") search_button = find_by(id_='su') def search(self, keywords): self.search_box().clear() self.search_box().send_keys(keywords) self.search_button().click() if __name__ == '__main__': driver = webdriver.Chrome() driver.get("https://www.baidu.com") BaiduSearchPage(driver).search("selenium") driver.close() 同样封装了8种定位方法: id_ (为避免与内置的关键字ID冲突,所以多了个下划线的后缀) name class_name css_selector tag_name xpath link_text partial_link_text 当然,这只是PageFactory 模式的一种表现形式而已。除此之外,我还找到了另外一个PageFactory模式的例子。 https://github.com/mattfair/SeleniumFactory-for-Python 这哥们是利用PageFactory模式把驱动的创建做了封装,感兴趣可以了解一下。 搬运完了,准备过年。新年快了~!!!
天变冷了,人也变得懒了不少,由于工作的需要,最近一直在学习CodeIgniter(CI)框架的使用,没有系统的从PHP基本语法学起,在网上靠百度谷歌,东拼西凑的实现了一些简单的功能。所以,老PHPer可以绕道了。 PHP实现简易blog 参考该篇博客所实现的功能,重新用CI实现了一下。 主要实现文章的添加、查看、删除、搜索。这里面最难实现的是文章分页,看似简单的功能,却费了一些功夫。 当然,离一个完整的系统还有很多功能没开发,这里只是简单引用了bootstrap的样式。 MVC模型 CI遵循于MVC模型,如果接触过其它基于MVC模型的web框架的话,理解起来还是比较简单的。 web的开发主要就是在这三个目录下进行。 控制器(controllers目录) 是模型、视图以及其他任何处理 HTTP 请求所必须的资源之间的中介,并生成网页。 模型(models目录) 代表你的数据结构。通常来说,模型类将包含帮助你对数据库进行增删改查的方法。 视图(views目录) 是要展现给用户的信息。一个视图通常就是一个网页,但是在 CodeIgniter 中, 一个视图也可以是一部分页面(例如页头、页尾),它也可以是一个 RSS 页面, 或其他任何类型的页面。 注:本文中的CI运行基于WAMPServer 环境。 创建模型 打开phpMyAdmin创建表。 这里主要基于该表设计,表名为“myblog”。 在.../application/config/database.php 添加数据库连接。 mysql默认密码为空,数据库名为“test”。“myblog”表在“test”库下创建。 下面实现数据模型层代码,主要是以CI的规则来操作数据库。 .../application/models/News_model.php <?php class News_model extends CI_Model { public function __construct() { $this->load->database(); } //获取所有blog public function blogs($w,$num,$offset) { if($w == 1) { $query = $this->db->get('myblog',$num,$offset); return $query->result_array(); }elseif(strpos($w,"title like")) { $query = $this->db->query("select * from myblog where $w order by id desc limit 5;"); return $query->result_array(); }else{ $query = $this->db->get('myblog',$num,$offset); return $query->result_array(); } } //查看一篇blog public function up_blogs($id = FALSE) { if ($id === FALSE) { $query = $this->db->get('myblog'); return $query->result_array(); } //更新点击数 $this->db->query("update myblog set hits=hits+1 where id='$id';"); $query = $this->db->get_where('myblog', array('id' => $id)); return $query->row_array(); } //添加一篇blog public function add_blogs() { $this->load->helper('url'); //$slug = url_title($this->input->post('title'), 'dash', TRUE); $d = date("Y-m-d"); $data = array( 'title' => $this->input->post('title'), 'dates' => $d, 'contents' => $this->input->post('text') ); return $this->db->insert('myblog', $data); } //删除一篇blog public function del_blogs($id = FALSE){ $this->load->helper('url'); if ($id === FALSE) { $query = $this->db->get('myblog'); return $query->result_array(); } $array = array( 'id' => $id ); return $this->db->delete("myblog",$array); } } 创建控制 控制层一直起着承前启后的作用,前是前端页面,后是后端数据库。 .../application/controllers/News.php <?php class News extends CI_Controller { public function __construct() { parent::__construct(); $this->load->model('news_model'); $this->load->helper('url_helper'); } public function index() { $this->load->library('calendar'); //加载日历类 parse_str($_SERVER['QUERY_STRING'], $_GET); $this->load->library('pagination');//加载分页类 $this->load->model('news_model');//加载books模型 $res = $this->db->get('myblog');//进行一次查询 $config['base_url'] = base_url().'index.php/news/index';//设置分页的url路径 $config['total_rows'] = $res->num_rows();//得到数据库中的记录的总条数 $config['per_page'] = '3';//每页记录数 $config['prev_link'] = 'Previous '; $config['next_link'] = ' Next'; $this->pagination->initialize($config);//分页的初始化 if (!empty($_GET['key'])) { $key = $_GET['key']; $w = " title like '%$key%'"; }else{ $w=1; } $data['blogs'] = $this->news_model->blogs($w,$config['per_page'],$this->uri->segment(3));//得到数据库记录 $this->load->view('templates/header'); $this->load->view('news/index', $data); $this->load->view('templates/footer'); } public function view($id = NULL) { $this->load->library('calendar'); $data['blogs_item'] = $this->news_model->up_blogs($id); if (empty($data['blogs_item'])) { show_404(); } $data['title'] = $data['blogs_item']['title']; $this->load->view('templates/header'); $this->load->view('./news/view', $data); $this->load->view('templates/footer'); } public function del($id = NULL) { $this->news_model->del_blogs($id); //通过js跳回原页面 echo' <script language="javascript"> alert("create success!"); window.location.href="http://localhost/CI_blog/index.php/news/"; </script> '; } public function create() { $this->load->library('calendar'); //加载日历类 $this->load->helper('form'); $this->load->library('form_validation'); $data['title'] = 'Create a news item'; $this->form_validation->set_rules('title', 'Title', 'required'); $this->form_validation->set_rules('text', 'Text', 'required'); if ($this->form_validation->run() === FALSE) { $this->load->view('templates/header', $data); $this->load->view('news/create'); $this->load->view('templates/footer'); } else { $this->news_model->add_blogs(); //跳回blog添加页面 echo' <script language="javascript"> alert("create success!"); window.location.href="http://localhost/CI_blog/index.php/news/create"; </script> '; } } } 创建视图 为了让页面好看,使用了bootstrap。 定义页头: .../application/views/templates/header.php <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" href="../../favicon.ico"> <title>Blog Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"> <link href="//v3.bootcss.com/examples/blog/blog.css" rel="stylesheet"> <script src="//v3.bootcss.com/assets/js/ie-emulation-modes-warning.js"></script> </head> 定义页尾: .../application/views/templates/footer.php <footer class="blog-footer"> <p>Blog template built for <a href="http://getbootstrap.com">Bootstrap</a> by <a href="https://twitter.com/mdo">@mdo</a>.</p> <p> <a href="#">Back to top</a> </p> </footer> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="//v3.bootcss.com/assets/js/ie10-viewport-bug-workaround.js"></script> </body> </html> 不过,这里使用的bootstrap并非引用的本地。而是使用的CDN加速点。 blog首页 .../application/views/news/index.php <body> <div class="blog-masthead"> <div class="container"> <nav class="blog-nav"> <a class="blog-nav-item active" href="#">Blog</a> <a class="blog-nav-item" href="//localhost/CI_blog/index.php/news/create">Create</a> <a class="blog-nav-item" href="#">Press</a> <a class="blog-nav-item" href="#">New hires</a> <a class="blog-nav-item" href="//localhost/CI_blog/index.php/login">Login</a> <form class="navbar-form navbar-right" method="get"> <div class="form-group"> <input type="text" name="key" placeholder="sreach" class="form-control"> </div> <button type="submit" class="btn btn-success">Srecch</button> </form> </nav> </div> </div> <div class="container"> <div class="row"> <div class="col-sm-8 blog-main"> <div class="blog-post"> <br> <?php foreach ($blogs as $blogs_item): ?> <h2 class="blog-post-title"><?php echo $blogs_item['title']; ?></h2> <p class="blog-post-meta"> <?php echo $blogs_item['dates']; ?> Reading:<?php echo $blogs_item['hits']; ?></a> </p> <div class="main"> <?php echo iconv_substr($blogs_item['contents'],0,100); ?>... </div> <p><a href="<?php echo site_url('news/view/'.$blogs_item['id']); ?>">View article</a></p> <p><a href="<?php echo site_url('news/del/'.$blogs_item['id']); ?>">Delete</a></p> <?php endforeach; ?> <!--翻页链接--> <br><br><?php echo $this->pagination->create_links();?> </div><!-- /.blog-post --> </div><!-- /.blog-main --> <div class="col-sm-3 col-sm-offset-1 blog-sidebar"> <div class="sidebar-module sidebar-module-inset"> <h4>About</h4> <p>Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p> </div> <div class="sidebar-module sidebar-module-inset"> <?php echo $this->calendar->generate(); ?> </div> <div class="sidebar-module"> <h4>Archives</h4> <ol class="list-unstyled"> <li><a href="#">March 2014</a></li> <li><a href="#">February 2014</a></li> <li><a href="#">January 2014</a></li> <li><a href="#">December 2013</a></li> <li><a href="#">November 2013</a></li> <li><a href="#">October 2013</a></li> <li><a href="#">September 2013</a></li> <li><a href="#">August 2013</a></li> <li><a href="#">July 2013</a></li> <li><a href="#">June 2013</a></li> <li><a href="#">May 2013</a></li> <li><a href="#">April 2013</a></li> </ol> </div> <div class="sidebar-module"> <h4>Elsewhere</h4> <ol class="list-unstyled"> <li><a href="#">GitHub</a></li> <li><a href="#">Twitter</a></li> <li><a href="#">Facebook</a></li> </ol> </div> </div><!-- /.blog-sidebar --> </div><!-- /.row --> </div><!-- /.container --> blog添加页面 .../application/views/news/create.php <!-- ckeditor编辑器源码文件 --> <script src="//cdn.ckeditor.com/4.5.5/standard/ckeditor.js"></script> <body> <div class="blog-masthead"> <div class="container"> <nav class="blog-nav"> <a class="blog-nav-item" href="//localhost/CI_blog/index.php/news">Blog</a> <a class="blog-nav-item active" href="#">Create</a> <a class="blog-nav-item" href="#">Press</a> <a class="blog-nav-item" href="#">New hires</a> <a class="blog-nav-item" href="#">About</a> </nav> </div> </div> <div class="container"> <div class="blog-header"> <h2 class="blog-title"><?php echo $title; ?></h2> <p class="lead blog-description">Please add an article.</p> </div> <div class="row"> <div class="col-sm-8 blog-main"> <div class="blog-post"> <?php echo validation_errors(); ?> <?php echo form_open('news/create'); ?> <label for="title">Title</label><br/> <input type="input" name="title" /><br/> <label for="text">Contents</label><br/> <textarea rows="10" cols="80" name="text"></textarea><br/> <script type="text/javascript">CKEDITOR.replace('text');</script> <input type="submit" name="submit" value="Create news item" /> </form> </div><!-- /.blog-post --> </div><!-- /.blog-main --> <div class="col-sm-3 col-sm-offset-1 blog-sidebar"> <div class="sidebar-module sidebar-module-inset"> <h4>About</h4> <p>Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p> </div> <div class="sidebar-module sidebar-module-inset"> <?php echo $this->calendar->generate(); ?> </div> <div class="sidebar-module"> <h4>Archives</h4> <ol class="list-unstyled"> <li><a href="#">March 2014</a></li> <li><a href="#">February 2014</a></li> <li><a href="#">January 2014</a></li> <li><a href="#">December 2013</a></li> <li><a href="#">November 2013</a></li> <li><a href="#">October 2013</a></li> <li><a href="#">September 2013</a></li> <li><a href="#">August 2013</a></li> <li><a href="#">July 2013</a></li> <li><a href="#">June 2013</a></li> <li><a href="#">May 2013</a></li> <li><a href="#">April 2013</a></li> </ol> </div> <div class="sidebar-module"> <h4>Elsewhere</h4> <ol class="list-unstyled"> <li><a href="#">GitHub</a></li> <li><a href="#">Twitter</a></li> <li><a href="#">Facebook</a></li> </ol> </div> </div><!-- /.blog-sidebar --> </div><!-- /.row --> </div><!-- /.container --> 这里使用了ckeditor 编辑器的使用我们可以创建带格式的文章。同样引的CDN。 最后,还要在routes.php文件中添加以下配置。 .../application/config/routes.php $route['news/create'] = 'news/create'; $route['news/view'] = 'news/view/$1'; $route['news/del'] = 'news/del/$1'; $route['news/news'] = 'news'; $route['news'] = 'news'; $route['default_controller'] = 'pages/view'; 好了,主要代码就这些了。感兴趣去github上看完整代码吧! https://github.com/defnngj/ci_blog PS:这只是为了练习而已,所以,各个功能很乱,并无打算写一个完整的系统。
认识常见编码 GB2312是中国规定的汉字编码,也可以说是简体中文的字符集编码 GBK 是 GB2312的扩展 ,除了兼容GB2312外,它还能显示繁体中文,还有日文的假名 cp936:中文本地系统是Windows中的cmd,默认codepage是CP936,cp936就是指系统里第936号编码格式,即GB2312的编码。 (当然有其它编码格式:cp950 繁体中文、cp932 日语、cp1250 中欧语言。。。) Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。 UTF-8 (8-bit Unicode Transformation Format)是最流行的一种对 Unicode 进行传播和存储的编码方式。它用不同的 bytes 来表示每一个代码点。ASCII 字符每个只需要用一个 byte ,与 ASCII 的编码是一样的。所以说 ASCII 是 UTF-8 的一个子集。 在开发Python程序的过程中,会涉及到三个方面的编码: Python程序文件的编码 Python程序运行时环境(IDE)的编码 Python程序读取外部文件、网页的编码 Python程序文件的编码 例如: Python2自带的IDE,当创建了一个文件保存的时候提示: 这是因为Python2编辑器默认的编码是ASCII,它是无法识别中文的,所以会弹出这样的提示。这也是我们在大多情况下写python2程序的时候习惯在程序的第一行加上:#coding=utf-8 其实,这里的编码文件是很容易解决的。 Python程序运行时环境(IDE)的编码 执行下面的一段程序。 #coding=utf-8 from selenium import webdriver driver = webdriver.Firefox() driver.get("http://www.baidu.com") # 返回百度页面底部备案信息 text = driver.find_element_by_id("cp").text print(text) driver.close() 在windows cmd下执行: 我们要获取的信息是: ©2015 Baidu 使用百度前必读 意见反馈 京ICP证030173号 Windows cmd 用的是cp936,也就是中文的GB2312,在GBK的字符集里没有“©”,这就导致通过GBK解析的时候出现编码问题。 这就像你在翻译英文的时候,出现了一个单词,这个单词你查遍了牛津大词典都没找到对应的含义解释,那么自然是会有问题的。 那假设,我还就想在cmd下执行这个python程序了,那么可以去修改cmd的默认编码类型为utf-8,对应的编码为CHCP 65001(utf-8)。在cmd 下输入:chcp 65001 命令回车。 然后,修改cmd的字体为“Lucida Console”,再来执行程序就可以被正确输出了。 Python程序读取外部文件、网页的编码 #这一块,暂时没有找到合适的例子 查看Python系统编码 查看Python2 或Python3的系统编码。 Python2: Python 2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)] on win32 Type "copyright", "credits" or "license()" for more information. >>> import sys >>> sys.getdefaultencoding() 'ascii' Python3: Python 3.5.0 (v3.5.0:374f501f4567, Sep 13 2015, 02:27:37) [MSC v.1900 64 bit (AMD64)] on win32 Type "copyright", "credits" or "license()" for more information. >>> import sys >>> sys.getdefaultencoding() 'utf-8' 那么如何修改Python2的系统编码为urf-8呢? import sys reload(sys) sys.setdefaultencoding('utf-8') 所以,在你的程序执行的过程中,遇到下面的报错信息时。 UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1.... 可以将上面的三行代码加到Python程序的头部。 decode()与encode() decode 的作用是将其他编码的字符串转换成 Unicode 编码,eg name.decode(“GB2312”),表示将GB2312编码的字符串name转换成Unicode编码。 encode 的作用是将Unicode编码转换成其他编码的字符串,eg name.encode(”GB2312“),表示将GB2312编码的字符串name转换成GB2312编码。 例如,前面获取百度底部信息的例子。我还可以通过decode()与encode()来解决: #coding=utf-8 from selenium import webdriver driver = webdriver.Chrome() driver.get("http://www.baidu.com") # 返回百度页面底部备案信息 text = driver.find_element_by_id("cp").text text2 = text.encode("gbk","ignore").decode("gbk") print(text2) 这里通过encode()将Unicode编码转换成gbk编码,在转换的过程中通过“ignore”忽略掉gbk不能识别的字符(©),然后再把gbk转换成Unicode编码。当然,这并不是一种完美的方式,毕竟牺牲部分字符串。 chardet模块 chardet是一个非常优秀的编码识别模块。 通过pip 安装: >pip install chardet 使用: >>> from chardet import detect >>> a = "中文" >>> detect(a) {'confidence': 0.682639754276994, 'encoding': 'KOI8-R'} 大概有68%的把握为KOI8-R编码类型。
robotremoteserver 是什么? Python Remote Server for Robot Framework 下载地址:https://pypi.python.org/pypi/robotremoteserver/ robotremoteserver是一种远程库接口技术(remote library interface)。其实,通过这两天的使用,我的理解它就是一个远程库的容器。这看上去有点不太好理解,我们知道当我要使用的Robot Framework的库是被安装在..\Python27\Lib\site-packages\目录下面的。例如常用的Selenium2Library。 但robotremoteserver就可以启动一个Library给Robot Framework用,不管这个库在本机的任何位置,或远程的某台主机上,或者这个库不是Python开发的。这听上去有点意思,对吧! 如何使用robotremoteserver 通过上面的连接将robotremoteserver 下载下来,注意不要使用pip安装,它其实也就只有一个robotremoteserver.py文件,我们需要的也就是这个文件而已。 先来体验一下它的用法。 首先创建一个目录E:\rfremote\ ,目录名你可以随便取。然后,将robotremoteserver.py拷贝到该目录下。接着在该目录下创建CountLibrary.py文件。 #coding=utf-8 import sys from robotremoteserver import RobotRemoteServer class CountLibrary: def add(a,b): '''Computing a and b are two numbers together, for example: | add | 2 | 5 | ''' return a + b def sub(a,b): '''Computing a and b subtract two numbers, for example: | sub | 10 | 2 | ''' return a - b if __name__ == '__main__': CL = CountLibrary() RobotRemoteServer(CL, *sys.argv[1:]) 代码很简单,创建了一个计算类CountLibrary。实现了add()和sub()两个方法,用于计算两个的加法和减法。最后将CountLibrary放到RobotRemoteServer中。 通过python命令执行该CountLibrary.py文件 现在,启动Robot Framework RIDE,导入“Remote”库。 按键盘F5 ,就可以看到Remote库中的“关键字”了。 看!现在你就可以使用。Add 和 Sub 两个关键字了,Stop Remote Server 是由robotremoteserver提供的,用户关闭库的容器。 然而,这貌似没有什么卵用。我为什么不把CountLibrary.py放到..\Python27\Lib\site-packages\目录下面调用呢!? 远程调用robotremoteserver 如果你细心会看到,刚才使用python命令启动CountLibrary.py的时候,启动是一个Remote server 并且指定127.0.0.1:8270 本机。 那么这个Library其实也可以在远程的某台主机上启动。 下面把整个rfremote\目录拷贝到虚拟机或远程某台主机。通过“ipconfig”或“ifconfig”查看IP地址。我们假设远程的这台主机的IP是:192.168.31.179 。 打开robotremoteserver.py修改host: …… class RobotRemoteServer(SimpleXMLRPCServer): allow_reuse_address = True _generic_exceptions = (AssertionError, RuntimeError, Exception) _fatal_exceptions = (SystemExit, KeyboardInterrupt) def __init__(self, library, host='192.168.31.179', port=8270, port_file=None, allow_stop=True): …… 好了!现在你的远程主机上通过python命令启动CountLibrary.py文件。 然后,在本机上再次启动Robot Framework RIDE 因为是远程库,所以,在引入这个库时要指定它是远程的IP和端口号。 然后,这依然没有什么卵用。下面就用它做点有卵用的事儿。 调用Sikuli 关于sikuli的介绍,请参考:http://www.cnblogs.com/fnng/archive/2012/12/15/2819367.html 这是一种另类的自动化技术,有它的缺点,也有它的优,如果能与现有的Robot Framework工具结合,无疑是比较牛X的说。 那么问题来了,sikuli虽然内部使用了python开发(也不是全python),但它是个jar包,也就是说它是由Java打包,只能给java调用。而Robot Framework是由纯python开发,只能引用python开发的库。虽然关系有点乱。但你要知道他们不是同类。 Jython是Python与Java 之间的红娘。Jython基于jvm虚拟机开发的Python语法。通过它可以调用Java程序或Java的标准库。 Jython下载地址:http://www.jython.org 安装(需要有java环境): > java -jar jython-installer-2.7.0.jar 使用Jython 其实,Jython也可以当Python用,我们一般用的python是基于C实现的,而Jython是基于JVM实现的python,基于JVM的语言很多,比如Groovy 、JRuby 等。 得到sikuli-script.jar 包,它可以看作是sikuli的核心模块。 两种方法: 单独下载:http://download.csdn.net/download/hqd1986/4557974 安装sikuli http://www.sikuli.org/downloadrc3.html ,在安装目录下找到sikuli-script.jar 文件。然后将其拷贝到E:\rfremote\ 目录并解压。 接下来在rfremote\目录下创建SikuliLibrary.py文件。 import sys from robotremoteserver import RobotRemoteServer from org.sikuli.script import * class SikuliLibrary: def __init__(self): self.SS = Screen() self.PT = Pattern() def _wait(self, imgFile, timeOut, similarity): try: self.PT = Pattern(imgFile) self.PT = self.PT.similar(float(similarity)) self.SS.wait(self.PT, float(timeOut)) except FindFailed, err: print "ERR: _wait" raise AssertionError(err) def click_object(self, imgFile, timeOut, similarity): try: self._wait(imgFile, timeOut, similarity) self.SS.click(imgFile) except FindFailed, err: raise AssertionError("Cannot click [" + imgFile + "]") def object_exists(self, imgFile, similarity, timeOut): try: self._wait(imgFile, timeOut, similarity) except FindFailed, err: raise AssertionError("Could not find [" + imgFile + "]") def type_at_object(self, imgFile, txt, timeOut, similarity): try: self._wait(imgFile, timeOut, similarity) self.SS.type(imgFile, txt) except FindFailed, err: raise AssertionError("Cannot type at [" + imgFile + "]") def paste_at_object(self, imgFile, txt, timeOut, similarity): try: self._wait(imgFile, timeOut, similarity) self.SS.paste(imgFile, txt) except FindFailed, err: raise AssertionError("Cannot paste at [" + imgFile + "]") if __name__ == '__main__': SL = SikuliLibrary() RobotRemoteServer(SL, *sys.argv[1:]) 这个程序是关键,通过Jython第调用了org.sikuli.script.* 中的方法重新实现。可以理解成,调用java程序,重新实现成python程序,然后给python程序使用。 这一次用需要使用Jython运行该文件。 然后,再次启动Robot Framework RIDE 把要操作的对象截好图: 然后,在Robot Framework中调用这些图片。 过程很简单,就是点击“开始”菜单,打开chrome浏览器。
最近,有时间看了点PHP的代码。参考PHP100教程做了简单的blog,网易云课堂2012年的教程,需要的可以找一下,这里面简单的记录一下。 首先是集成环境,这里选用的WAMP:http://www.wampserver.com/en/ 首先通过,phpMyAdmin创建一张blog表。 纯界面操作,过程比较简单,需要注意的是id是主键,并且设置auto_increnent 选项,表示该字段为空时自增。其它字段就比较随便了,注意类型和长度即可。 创建数据连接 在./wamp/www/blog目录下创建conn.php文件。 <?php @mysql_connect("127.0.0.1:3306","root","") or die("mysql数据库连接失败"); @mysql_select_db("test")or die("db连接失败"); mysql_query("set names 'gbk'"); ?> mysql默认用户名为root,密码为空,这里创建的blog在test库中,所以需要连接test库。 添加blog 在./wamp/www/blog/目录下创建add.php文件。 <a href="index.php"><B>index</B></a> <a href="add.php"><B>add blog</B></a> <hr> <?php include("conn.php"); //引入连接数据库 if (!empty($_POST['sub'])) { $title = $_POST['title']; //获取title表单内容 $con = $_POST['con']; //获取contents表单内容 $sql= "insert into blog values(null,'0','$title',now(),'$con')"; mysql_query($sql); echo "insert success!"; } ?> <form action="add.php" method="post"> title :<br> <input type="text" name="title"><br><br> contents:<br> <textarea rows="5" cols="50" name="con"></textarea><br><br> <input type="submit" name="sub" value="submit"> </form> 这段代码分两部分,上部分是PHP代码,include (或 require)语句会获取指定文件中存在的所有文本/代码/标记,并复制到使用 include 语句的文件中。 然后,判断表单中name=’sub’的内容不为空的情况下,将获取表单的内容,然后执行$sql 语句,null 表示id为空(自增),now()表示取当前日起,$title和$con取表单中用户提交的内容。最后eche 插入成功的提示。 下半部分就是一段简单的HTML代码了,用于实现一个可以blog表单提交的功能。 创建blog的首页 在./wamp/www/blog/目录下创建index.php文件。 <a href="index.php"><B>index</B></a> <a href="add.php"><B>add blog</B></a> <br><br> <form action="" method="get" style='align:"right"'> <input type="text" name="keys" > <input type="submit" name="subs" > </form> <hr> <?php include("conn.php"); //引入连接数据库 if (!empty($_GET['keys'])) { $key = $_GET['keys']; $w = " title like '%$key%'"; }else{ $w=1; } $sql ="select * from blog where $w order by id desc limit 5"; $query = mysql_query($sql); while ($rs = mysql_fetch_array($query)) { ?> <h2>title: <a href="view.php?id=<?php echo $rs['id']; ?>"><?php echo $rs['title']; ?></a> | <a href="edit.php?id=<?php echo $rs['id']; ?>">edit</a> | <a href="del.php?id=<?php echo $rs['id']; ?>">delete</a> | </h2> <li>date: <?php echo $rs['data']; ?></li> <!--截取内容展示长度--> <p>contents:<?php echo iconv_substr($rs['contents'],0,30,"gbk"); ?>...</p> <hr> <?php }; ?> 该页面包含有的功能还是比较多的。 首先是一个搜索表单,通过if判断搜索表单的内容是否为空,如果不为空,通过输入关键字匹配文章的标题并显示结果;如果为空查询所有blog内容,并循环显示每一篇文章的标题、日期、正文。点击标题会链接到该篇blog的详细页面。每一篇文章提供“编辑”和“删除”功能。 mysql_query()用于执行sql语句。mysql_fetch_arry()将返回的数据生成数组,这样就可以像操作数组一样,操作数据库中的每一条数据了。 然后是正文的显示,通过 iconv_substr() 函数提取正文前30个字符。 查看blog 在./wamp/www/blog/目录下创建view.php文件。 <a href="index.php"><B>index</B></a> <a href="add.php"><B>add blog</B></a> <hr> <?php include("conn.php"); //引入连接数据库 if (!empty($_GET['id'])) { $id = $_GET['id']; $sql ="select * from blog where id='$id' "; $query = mysql_query($sql); $rs = mysql_fetch_array($query); $sqlup = "update blog set hits=hits+1 where id='$id'"; mysql_query($sqlup); } ?> <h2>title: <?php echo $rs['title']; ?> </h1> <h3>date: <?php echo $rs['data']; ?> click number: <?php echo $rs['hits']; ?></h3> <hr> <p>contents:<?php echo $rs['contents']; ?></p> blog的正文实现比较简单,通过get请求获取blog的id,然后通过sql语句将该id对应的标题、日期和正文查询出来并显示。 并外一个小功能是显示了一个简单的计数器,每刷新页面,点击数加1。 编辑blog 在./wamp/www/blog/目录下创建edit.php文件。 <a href="index.php"><B>index</B></a> <a href="add.php"><B>add blog</B></a> <hr> <?php include("conn.php"); //引入连接数据库 //获取数据库表数据 if (!empty($_GET['id'])) { $edit = $_GET['id']; $sql = "select * from blog where id='$edit'"; $query = mysql_query($sql); $rs = mysql_fetch_array($query); } //更新数据库表数据 if (!empty($_POST['sub'])) { $title = $_POST['title']; //获取title表单内容 $con = $_POST['con']; //获取contents表单内容 $hid = $_POST['hid']; $sql= "update blog set title='$title', contents='$con' where id='$hid' "; mysql_query($sql); echo "<script>alert('update success.');location.href='index.php'</script>"; } ?> <form action="edit.php" method="post"> <input type="hidden" name="hid" value="<?php echo $rs['id'];?>"> title :<br> <input type="text" name="title" value="<?php echo $rs['title'];?>"> <br><br> contents:<br> <textarea rows="5" cols="50" name="con" ><?php echo $rs['contents'];?></textarea><br><br> <input type="submit" name="sub" value="submit"> </form> 编辑blog的功能相对复杂一些。分两部操作,第一步先将blog的标题和正文查询出来,并显示到输入框。第二步将编辑好的内容再更新到数据库中。 删除blog 在./wamp/www/blog/目录下创建del.php文件。 <a href="index.php"><B>index</B></a> <a href="add.php"><B>add blog</B></a> <hr> <?php include("conn.php"); //引入连接数据库 if (!empty($_GET['id'])) { $del = $_GET['id']; //删除blog $sql= "delete from blog where id='$del' "; mysql_query($sql); echo "delete success!"; } ?> 最后是实现blog的删除功能,通过id将该条blog的查询出来并显示。 因为所有页面没有使用前端样式有美化,很丑就不贴图了。功能还算完美。在此记录,算做PHP学习的整理。 ======================================================= 另外,虽然每个语言都有优缺点,这里还是忍不住要吐槽一下PHP的两个不好之处。 1、符号不好写, “$” 、“ ->” 、 “=>”。这些符号虽然并没有增加代码语法的理解难度。但敲起来具恶心。每次在打“$”符号的时候,都要眼看键盘按着shift键找4在哪儿。 2、php与html的混编在我看来也不是太优雅。
在编写Web自动化测试用例的时候,如何写断言使新手不解,严格意义上来讲,没有断言的自动化脚本不能叫测试用例。就像功能测试一样,当测试人员做了一些操作之后必然会判断实际结果是否等于预期结果,只不过,这个过程由测试人员的眼睛完成。而自动化测试脚本必然要通过一此信息来断定用例是否成功。 这其中常用的三种信息分别是: title :页面不同或显示不同时往往title也会有所变化。 url :与title类似,当页面发生变化时,跟着url也会改变。 text:相比前者应用更广泛,因为通过它可以获取页面上的任意标识性文本,用于“证明”用例执行是成功的。例如,登陆之后人用户名,查询的结果等。 但是,在有些情况下,无法获取这些信息来证明用例是成功的怎么办?当然,下策是不写断言,脚本运行没有报错来证明用例执行成功,这当然是无奈之举。除此之外还可以选择断言两张图片,在用例执行正确的情况下对当前页面进行截图,在用例执行的过程中再次进行截图。通过对两张图片进行比较,从而判断用例是否运行成功。 Pillow下载:https://pypi.python.org/pypi/Pillow/3.0.0 根据自己的操作系统以及python版本选择下载。 安装: > python3 -m pip install Pillow-3.0.0-cp35-none-win_amd64.whl Processing c:\selenium\pillow-3.0.0-cp35-none-win_amd64.whl Installing collected packages: Pillow Successfully installed Pillow-3.0.0 注意,因为我本机同时安装了Python2.7和Python3.5,所以,这里特意指定安装在Python3的下面。 from PIL import Image import math import operator from functools import reduce def image_contrast(img1, img2): image1 = Image.open(img1) image2 = Image.open(img2) h1 = image1.histogram() h2 = image2.histogram() result = math.sqrt(reduce(operator.add, list(map(lambda a,b: (a-b)**2, h1, h2)))/len(h1) ) return result if __name__ == '__main__': img1 = "./img1.jpg" # 指定图片路径 img2 = "./img2.jpg" result = image_contrast(img1,img2) print(result) 如果两张图片完全相等,则返回结果为浮点类型“0.0”,如果不相同则返回结果值越大。 这样就可以在自动化测试用例中调用该方法来断言执行结果。 =====================
参考官网文档,创建投票系统。 ================ Windows 7/10 Python 2.7.10 Django 1.8.2 ================ 1、创建项目(mysite)与应用(polls) D:\pydj>django-admin.py startproject mysite D:\pydj>cd mysite D:\pydj\mysite>python manage.py startapp polls 添加到setting.py # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'polls', ) 最终哪个目录结构: 2、创建模型(即数据库) 一般web开发先设计数据库,数据库设计好了,项目就完了大半了,可见数据库的重要性。打开polls/models.py编写如下: # coding=utf-8 from django.db import models # Create your models here. # 问题 class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') def __unicode__(self): return self.question_text # 选择 class Choice(models.Model): question = models.ForeignKey(Question) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __unicode__(self): return self.choice_text 执行数据库表生成与同步。 D:\pydj\mysite>python manage.py makemigrations polls Migrations for 'polls': 0001_initial.py: - Create model Question - Create model Choice - Add field question to choice D:\pydj\mysite>python manage.py syncdb …… You have installed Django's auth system, and don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (leave blank to use 'fnngj'): 用户名(默认当前系统用户名) Email address: fnngj@126.com 邮箱地址 Password: 密码 Password (again): 重复密码 Superuser created successfully. 3、admin管理 django提供了强大的后台管理,对于web应用来说,后台必不可少,例如,当前投票系统,如何添加问题与问题选项?直接操作数据库添加,显然麻烦,不方便,也不安全。所以,管理后台就可以完成这样的工作。 打开polls/admin.py文件,编写如下内容: from django.contrib import admin from .models import Question, Choice # Register your models here. class ChoiceInline(admin.TabularInline): model = Choice extra = 3 class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ] inlines = [ChoiceInline] list_display = ('question_text', 'pub_date') admin.site.register(Choice) admin.site.register(Question, QuestionAdmin) 当前脚本的作用就是将模型(数据库表)交由admin后台管理。 运行web容器: D:\pydj\mysite>python manage.py runserver Performing system checks... System check identified no issues (0 silenced). October 05, 2015 - 13:08:12 Django version 1.8.2, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CTRL-BREAK. 登录后台:http://127.0.0.1:8000/admin 登录密码就是在执行数据库同步时设置的用户名和密码。 点击“add”添加问题。 4、编写视图 视图起着承前启后的作用,前是指前端页面,后是指后台数据库。将数据库表中的内容查询出来显示到页面上。 编写polls/views.py文件: # coding=utf-8 from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse from .models import Question, Choice # Create your views here. # 首页展示所有问题 def index(request): # latest_question_list2 = Question.objects.order_by('-pub_data')[:2] latest_question_list = Question.objects.all() context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) # 查看所有问题 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) # 查看投票结果 def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/results.html', {'question': question}) # 选择投票 def vote(request, question_id): p = get_object_or_404(Question, pk=question_id) try: selected_choice = p.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, 'polls/detail.html', { 'question': p, 'error_message': "You didn't select a choice.", }) else: selected_choice.votes += 1 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. return HttpResponseRedirect(reverse('polls:results', args=(p.id,))) 5、配置url url是一个请求配置文件,页面中的请求转交给由哪个函数处理,由该文件决定。 首先配置polls/urls.py(该文件需要创建) from django.conf.urls import url from . import views urlpatterns = [ # ex : /polls/ url(r'^$', views.index, name='index'), # ex : /polls/5/ url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'), # ex : /polls/5/results/ url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'), # ex : /polls/5/vote url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ] 接着,编辑mysite/urls.py文件。 from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ url(r'^polls/', include('polls.urls', namespace="polls")), url(r'^admin/', include(admin.site.urls)), ] 6、创建模板 模板就是前端页面,用来将数据显示到web页面上。 首先创建polls/templates/polls/目录,分别在该目录下创建index.html、detail.html和results.html文件。 index.html {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} detail.html <h1>{{ question.question_text }}</h1> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <form action="{% url 'polls:vote' question.id %}" method="post"> {% csrf_token %} {% for choice in question.choice_set.all %} <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" /> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br /> {% endfor %} <input type="submit" value="Vote" /> </form> results.html <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a> 7、功能展示 启动web容器,访问:http://127.0.0.1:8000/polls/ ==========
关于HTTP协议,我考虑了一下觉得没必要再花一节内容来介绍,因为网上关于HTTP协议的介绍非常详细。本着以尽量避免介绍一空洞了概念与理论来介绍接口测试,我这里仍然会给出具体实例。 在此之前先简单的介绍一下基本概念:我们想要打开一个网站,首先是需要往浏览器的地址的URL输入框架中输入网地址。当我敲下回车后,通过HTTP协议,将网址传送到域名解析服务器,域名解析服务器根据网址找到对应的IP主机(系统服务器)。这个过程叫request,即请求;当IP主机拿到请求后,将相应的资源返回给用户浏览器。这个过程叫response,即响应。 当用户浏览器向系统服务器请求时,有几种方法,最常用的就是GET和POST两种方法。 在此我们来开发这样一个可以接收GET和POST请求的web应用。当然,这里就要求读者具备一定的web开发基础了。但不编程语言与web框架不是我们讨论的重点。 以flask框架的代码为例。 GET请求 pyfl/ |---- /hello.py |----/templates/ |----|-----------/index.html |----|-----------/user.html hello.py from flask import Flask,render_template app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") if __name__ == '__main__': app.run(debug=True) index.html <h1> This is index page <h1> 启动flask容器: 访问:http://127.0.0.1:5000/ 通过firebug查看GET请求信息: 当然,这个返回只是一个静态的页面,并且不需要任何参数,我们只需要判断返回是否为200即可。 扩充hello.py如下: from flask import Flask,render_template app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/user/<name>") def user(name): return render_template("user.html",name=name) if __name__ == '__main__': app.run(debug=True) user.html <h1> Hell, {{name}} !<h1> 访问:http://127.0.0.1:5000/user/aaa 相比较来说,这个GET请求就复杂了一些,在请求的时候跟了一些参数(aaa),后台(hello.py)对参数了进行了接收,并且将其反回到了user.html页面中。 这个时候,我们就可以对这个参数做一些简单的测试,比较参数为空,字符,数字,脚本,sql 之类的。其实,安全测试的sql注入也是通过输参中带入sql语句入手的。 POST请求 pyfl/ |---- /hello.py |----/templates/ |----|-----------/index.html hello.py from flask import Flask,render_template,request app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/login",methods = ['GET', 'POST']) def login(): if request.method == "POST": username = request.form.get('username') password = request.form.get('password') if username=="zhangsan" and password=="123": return "<h1>welcome, %s !</h1>" %username else: return "<h1>login Failure !</h1>" else: return "<h1>login Failure !</h1>" if __name__ == '__main__': app.run(debug=True) index.html <form action="/login" method="post"> username: <input type="text" name="username"> password: <input type="password" name="password"> <input type="submit" id="submit"> </form> 访问:http://127.0.0.1:5000/ 输入用户名,密码登录(后台hello.py判定,用户名为“zhangsan”,密码为“123”登录成功,其它帐号失败。) Python的有一个requests库,可以很方便的模拟测试POST请求。 #coding=utf-8 import requests s = requests data={"username":"zhangsan","password":"123",} r = s.post('http://127.0.0.1:5000/login', data) print r.status_code print r.headers['content-type'] print r.encoding print r.text 执行结果: 200 text/html; charset=utf-8 utf-8 <h1>welcome, zhangsan !</h1> POST接口的测试也一样,通过不输入为空,或错误的用户名密码,检查返回的内容。 =================== 本文算是入门,可讨论的问题还有很多,例如接口返回的是json格式的数据,例如接口为了安全加了数字签名。从测试的角度,有哪个工作可以模拟这些请求,如何组织和运行测试用例。后面有时间再讨论。
接口测试 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻辑依赖关系等。 --百度百科 为什么介绍接口测试? 接口常被开发挂在嘴边,在开发过程中无处不在,但对于测试人员来说,它又如此朦胧,无形无色无味,难以触碰。相信这也是测试人员比较难理解的一种测试类型。查询的大部分资料都是介绍一堆模糊的概念。所以,我打算以浅薄的认知来介绍接口测试,当然会举例子。 我所知道的接口测试 我所了解的模块接口测试大体分为两类:模块接口测试和web接口测试。 模块接口测试 模块接口测试是单元测试的基础。它主要测试模块的调用与返回。 package com.java.base; public class InterfaceTest { //求两个整数相加的和 public static int add(int a, int b){ return a + b; } public static void main(String[] args) { //测试整数相加 int resule = add(1,2); if(resule == 3){ System.out.println("success!"); }else{ System.out.println("fail!"); } } } 我擦!这明明是一个没有使用单元测试框架的单元测试嘛!但其实我们也可以把add()方法看成一个接口,这个接口接收两个整数并返回两个整数的和。 通过这个例子放大了来看,假如几个开发人员去完成一个系统,他们分别开发一些功能模块,最终数据会在这些功能模块之间传递。当A开发好自己负责的功能模块后会提供相应的接口(类方法、函数),B肯定需要模拟数据调用A写的接口,检查返回值是否正确。 当然,测试的手段遵循测试的一些要点。 1、检查接口返回的数据是否与预期结果一致。 2、检查接口的容错性,假如传递数据的类型错误时是否可以处理。例如上面的例子是支持整数,传递的是小数或字符串呢? 3、接口参数的边界值。例如,传递的参数足够大或为负数时,接口是否可以正常处理。 4、接口的性能,接口处理数据的时间也是测试的一个方法。牵扯到内部就是算法与代码的优化。 5、接口的安全性,如果是外部接口的话,这点尤为重要。 web接口测试 web接口测试又可分为两类:服务器接口测试和外部接口测试。 服务器接口测试:是测试浏览器与服务器的接口。这个很容易理解,我们知道web开发一般分前端和后端,前端开发人员用html/css/javascript等技术。后端开发人用php/java/python/ruby等各种语言。用户输入的数据是输入到的前端页面上,怎样把这些数据传递的后台的呢?通过http协议的get与post请求来实现前后端的数据传递。这也可认为是接口测试,调用的登录接口还是查询接口,传参的是用户密码还是搜索关键字。 外部接口测试:这个很典型的例子就是第三方登录,比如你做的新系统免于新用户重新注册的麻烦会提供第三方登录,那用户在登录的时候调用的就是第三方登录的接口,由第三方验证用户名和密码并且返回给当前系统。 对于服务器接口测试,我们来看例子。 ================================== 准备: Python 下载地址: https://www.python.org/ Flask 微型web框架。flask安装:> pip install flask ================================== 查看flsk是否安装成功。 来写一个例子hello.py: from flask import Flask app = Flask(__name__) @app.route("/user/<name>") def user(name): return "<h1>hello %s !</h1>" %name if __name__ == '__main__': app.run(debug=True) 运行hello.py 通过浏览器访问:http://127.0.0.1:5000/user/zhangsan 这是一个最简单的get请求,我们可以把“zhangsan”改成任意字符来时行测试。 对于web接口测试来说有哪些测试要点: 1、请求是否正确,默认请求成功是200,如果请求错误也能返回404、500等。 2、检查返回数据的正确性与格式;json是一种非常创建的格式。 3、接口的安全性,一般web都不会暴露在网上任意被调用,需要做一些限制,比如鉴权或认证。 4、接口的性能,web接口同样注重性能,这直接影响用户的使用体验。如果我搜索一个关键字半天结果都没返回,果断弃用。 =================== 讲这个话题我是比较冒险,对于接口测试我并没有全面的理解和认识。欢迎留言说出你对接口测试的理解。
对于测试来讲,不管是功能测试,自动化测试,还是单元测试。一般都会预设一个正确的预期结果,而在测试执行的过程中会得到一个实际的结果。测试的成功与否就是拿实际的结果与预期的结果进行比较。这个比的过程实际就是断言(assert)。 在unittest单元测试框架中提供了丰富的断言方法,例如assertEqual()、assertIn()、assertTrue()、assertIs()等,而pytest单元测试框架中并没提供特殊的断言方法,而是直接使用python的assert进行断言。 下面我们就来介绍assert 的使用。 比较大小与是否相等 test_assert.py #coding=utf-8 import pytest # 功能 def add(a,b): return a + b # 测试相等 def test_add(): assert add(3,4) == 7 # 测试不相等 def test_add2(): assert add(17,22) != 50 # 测试大于 def test_add3(): assert add(17,22) <= 50 # 测试小于 def test_add4(): assert add(17,22) >= 50 if __name__ == '__main__': pytest.main("test_assert.py") 定义一个add()函数,用于计算两个入参相加,并将相加的结果返回。 而assert可以使用直接使用“==”、“!=”、“<”、“>”、“>=”、"<=" 等符号来比较相等、不相等、小于、大于、大于等于和小于等于。 运行结果: ============================= test session starts ============================= platform win32 -- Python 2.7.10 -- py-1.4.30 -- pytest-2.7.2 rootdir: D:\pyse\pytest\test_case, inifile: plugins: html collected 4 items test_assert.py ...F ================================== FAILURES =================================== __________________________________ test_add4 __________________________________ def test_add4(): > assert add(17,22) >= 50 E assert 39 >= 50 E + where 39 = add(17, 22) test_assert.py:22: AssertionError ===================== 1 failed, 3 passed in 0.02 seconds ====================== 显然,17加22的结果并不大于50,所有最后一条用例失败。 测试包含或不包含 test_assert2.py #coding=utf-8 import pytest # 测试相等 def test_in(): a = "hello" b = "he" assert b in a # 测试不相等 def test_not_in(): a = "hello" b = "hi" assert b not in a if __name__ == '__main__': pytest.main("test_assert2.py") 通过定义a和b 字符串变量来比较包含的关系。 assert 可以直接使用 in 和not in 来比较包含与不包含。 运行结果: ============================= test session starts ============================= platform win32 -- Python 2.7.10 -- py-1.4.30 -- pytest-2.7.2 rootdir: D:\pyse\pytest\test_case, inifile: plugins: html collected 2 items test_assert2.py F. ================================== FAILURES =================================== ___________________________________ test_in ___________________________________ def test_in(): a = "hello" b = "hi" > assert b in a E assert 'hi' in 'hello' test_assert2.py:9: AssertionError ===================== 1 failed, 1 passed in 0.01 seconds ====================== 显然“hello”并不包含“hi”,所以第一条测试用例运行失败。 测试true或false test_assert3.py #coding=utf-8 import pytest #用于判断素数 def is_prime(n): if n <= 1: return False for i in range(2, n): if n % i == 0: return False return True # 判断是否为素数 def test_true(): assert is_prime(13) # 判断是否不为素数 def test_true(): assert not is_prime(7) if __name__ == '__main__': pytest.main("test_assert3.py") 通过is_prime()函数来判断n 是否为素数(只能被1和它本身整除的数)。返回值为ture或false。 通过assert不需要任何辅助符号,直接判断对象是否为ture,而assert not 用于判断是否为false。 运行结果: ============================= test session starts ============================= platform win32 -- Python 2.7.10 -- py-1.4.30 -- pytest-2.7.2 rootdir: D:\pyse\pytest\test_case, inifile: plugins: html collected 1 items test_assert3.py F ================================== FAILURES =================================== __________________________________ test_true __________________________________ def test_true(): > assert not is_prime(7) E assert not True E + where True = is_prime(7) test_assert3.py:22: AssertionError ========================== 1 failed in 0.01 seconds =========================== 显示,对于第二条测试用例来讲,7是素数,所以,is_prime()函数的返回结果是Ture,而assert not 需要的正确结果是False,因此,用例执行失败。
fixtures不太好翻译,可看作是夹心饼干最外层的两片饼干。通常用setup/teardown来表示。它主要用来包裹测试用例,为什么需要这样的饼干呢?我们以web自动化测试为例,例如,要测试的某系统需要登录/退出。那么每一条用例执行前都需要登录,执行完又都需要退出,这样每条用例重复编写登录和退出就很麻烦,当然,你也可以把登录和退出封装为方法调用,但是每个用例中都写调用也很麻烦。有了fixtures就变得简便很多。 测试函数 创建test_fixtures.py文件 #coding=utf-8 import pytest # 功能函数 def multiply(a,b): return a * b # =====fixtures======== def setup_module(module): print ("\n") print ("setup_module================>") def teardown_module(module): print ("teardown_module=============>") def setup_function(function): print ("setup_function------>") def teardown_function(function): print ("teardown_function--->") # =====测试用例======== def test_numbers_3_4(): print 'test_numbers_3_4' assert multiply(3,4) == 12 def test_strings_a_3(): print 'test_strings_a_3' assert multiply('a',3) == 'aaa' if __name__ == '__main__': pytest.main("-s test_fixtures.py") 运行结果: ============================= test session starts ============================= platform win32 -- Python 2.7.10 -- py-1.4.30 -- pytest-2.7.2 rootdir: D:\pyse\pytest, inifile: plugins: html collected 2 items test_fixtures.py setup_module================> setup_function------> test_numbers_3_4 .teardown_function---> setup_function------> test_strings_a_3 .teardown_function---> teardown_module=============> ========================== 2 passed in 0.01 seconds =========================== 通过执行结果,相信就很容易弄清楚它们的执行顺序。 setup_module/teardown_module 在所有测试用例执行之后和之后执行。 setup_function/teardown_function 在每个测试用例之后和之后执行。 测试类 #coding=utf-8 import pytest # 功能函数 def multiply(a,b): return a * b class TestUM: # =====fixtures======== def setup(self): print ("setup----->") def teardown(self): print ("teardown-->") def setup_class(cls): print ("\n") print ("setup_class=========>") def teardown_class(cls): print ("teardown_class=========>") def setup_method(self, method): print ("setup_method----->>") def teardown_method(self, method): print ("teardown_method-->>") # =====测试用例======== def test_numbers_5_6(self): print 'test_numbers_5_6' assert multiply(5,6) == 30 def test_strings_b_2(self): print 'test_strings_b_2' assert multiply('b',2) == 'bb' if __name__ == '__main__': pytest.main("-s test_fixtures.py") 运行结果: ============================= test session starts ============================= platform win32 -- Python 2.7.10 -- py-1.4.30 -- pytest-2.7.2 rootdir: D:\pyse\pytest, inifile: plugins: html collected 2 items test_fixtures.py setup_class=========> setup_method----->> setup-----> test_numbers_5_6 .teardown--> teardown_method-->> setup_method----->> setup-----> test_strings_b_2 .teardown--> teardown_method-->> teardown_class=========> ========================== 2 passed in 0.00 seconds =========================== setup_class/teardown_class 在当前测试类的开始与结束执行。 setup/treadown 在每个测试方法开始与结束执行。 setup_method/teardown_method 在每个测试方法开始与结束执行,与setup/treadown级别相同。
当Android SDK安装完成之后,并不意味着已经装好了安装模拟器。Android系统有多个版本,所以我们需要选择一个版本进行安装。 第三节 安装Android 模拟器 我这里以Android 4.4.2版本为例。 如上图,勾选所需要安装的工具,点击右下角“Install x packages...” 选择“Accept License”选项,点击“Install”按钮时行安装。 但是,你可能会发现这种方法会提示“Download interrupted: URL not found.”这样的错误,那么我们只能将这些工具单个的下载安装了。 好吧!再次感谢AndroidDevTools.cn网站的共享。以下下载链接均有其共享。 一、安装SDK platform android 4.4.2 :http://pan.baidu.com/s/1eQf8ZgI 这是Android开发所需的sdk,下载并解压后,将解压出的整个文件夹复制或者移动到 .../android-sdk-windows/platforms/文件夹,然后重新打开SDK Manager.exe 二,安装Samples for SDK android 4.4.2 : http://pan.baidu.com/s/1dDeSKt7 这是Android SDK自带的示例代码,下载并解压后,将解压出的整个文件夹复制或者移动到 .../android-sdk-windows/samples文件夹下,然后重启SDK Manager.exe。 三,安装SDK System images android 4.4.2 : http://pan.baidu.com/s/1i3Jwhed 这是在创建模拟器时需要的system image,也就是在创建模拟器时 CPU/ABI项需要选择的,下载并解压后,将解压出的整个文件夹复制或者移动到.../android-sdk-windows/system-images文件夹下即可, 如果没有 system-images目录就先创建此文件夹,然后重新打开SDK Manager.exe。 四,GoogleMap APIs SDK android 4.4.2 (ARM): http://pan.baidu.com/s/1bno0mFt android 4.4.2 (x86): http://pan.baidu.com/s/1jGgKyZc 这是GoogleMap APIs SDK,下载并解压后,将解压出的整个文件夹复制或者移动到 .../android-sdk-windows/add-ons文件夹下,然后打开SDK Manager 五,Android Framework Source Code android 4.4.2 : http://pan.baidu.com/s/1hqGGrVA 这是Android Framework Source Code,下载并解压后,将解压出的整个文件夹复制或者移动到.../android-sdk-windows/sources文件夹下,然后重新打开SDK Manager.exe。 为了保险起见,以防以后用到而没有安装,所以这里全部做了安装。 下面双击“AVD Manager.exe”创建android模拟器。 点击“Create....” 如果显示屏分辨率比较底的话,尽量选择低分辨率的“Device”。 点击“OK”,在AVD Manager 窗口,点击“Start...”按钮启动android 模拟器。 因为新虚拟机没了实体键,所以我们可以利用键盘按键来操作android虚拟机。 模拟器按键 键盘按键 后退 ESC 菜单 F1或Page Up 开始 F2或Page Down 呼叫 F3 挂断 F4 电源按钮 F7 禁止/启用所有网络 F8 开始跟踪 F9 停止跟踪 F10 旋转屏幕(横/竖屏切换) Ctrl+F11 主页 HOME 方向键 左/上/右/下 小键盘 4/8/6/2 方向键 中心键 小键盘 5 调低音量 小键盘 负号(-) 调高音量 小键盘 加号(+)
Appium 自动化测试是很时之前就想学习和研究的技术了,可是一直抽不出一块完整的时间来做这件事儿。现在终于有了。 反观各种互联网的招聘移动测试成了主流,如果再不去学习移动自动化测试技术将会被淘汰。 ==================== web自动化测试的路线是这样的:编程语言基础--->测试框架--->webdriver API--->开发自动化测试项目。 移动自动化的测试的路线要长一些:编程语言基础--->测试框架--->android/IOS开发测试基础---->appium API ----->开发移动自动化项目。 ===================== Appium测试环境的搭建相对比较繁琐,相信不少出学者都没开始学习就已经死在了环境搭建上。所以,我首先会分篇的介绍环境搭建的全过程。 1、一方面安装的东西多,另一方面受“墙”的干扰使这个过程会更麻烦些。 2、我这个过程中有些步骤不是必须要这么做的,我暂时讲不清所以然,但跟着我做你一定把环境搭建起来。 3、我的环境为win7 64,安装过程只适用我的环境。 第一节 安装Appium Appium官方网站:http://appium.io/ Easy setup process, run a test now. > brew install node # get node.js > npm install -g appium # get appium > npm install wd # get appium client > appium & # start appium > node your-appium-test.js 官方首页给出了appium的安装步骤。 所以,我们需要先安装node.js 。node.js官方网站:https://nodejs.org/ 根据你的操作系统选择相应的版本进行下载。这里我以Windows 7 (64) 为例进行安装,选择Windows installer(.msi) 64-bit 版本进行下载。 下载完成,双击进行安装,如下图。 安装完成,打开Windows 命令提示符,敲入“npm”命令回车。 如果出现如上图信息,表示node.js安装成功。 npm是一个node包管理和分发工具,已经成为了非官方的发布node模块(包)的标准。有了npm,可以很快的找到特定服务要使用的包,进行下载、安装以及管理已经安装的包。 下面通过npm安装Appium 。 --------------------------------------------- C:\Users\fnngj>npm install -g appium -- ----------------------------------------------------- 当然,这种方式的Appium 会很慢,为尊重官网上的介绍,而且大多Appium 相关资料也会介绍这种安装安装方式。 提示笔者缺少“VCBuid.exe”。 如果未安装该组件,请执行下列操作之一: 1)安装 Microsoft Windows SDK for Windows Server 2008 和 .NET Framework 3.5; 2) 安装 Microsoft Visual Studio 2008。 这是因为Appium是由.NET 开发的,所以,它会依赖 .NET framework相关组件。你当然可以按照提示下载安装1)或2)从而再次尝试安装Appium 。 但其实,我们可以在Appium官方网站上下载操作系统相应的Appium版本。 https://bitbucket.org/appium/appium.app/downloads/ 当前最新版本为AppiumForWindows_1_4_0_0.zip ,注意这是一个Windows 版本,如果你的电脑为MAC请下载appium-1.3.7.dmg。虽然你已经看到了这些下载包,但我不保证你能下载的下来。原因你懂的~! 所以,再来提供一个百度网盘的下载链接:http://pan.baidu.com/s/1jGvAISu 我们以Windows为例,将下载的AppiumForWindows_1_4_0_0.zip 进行解压,如下: 双击“appium-installer.exe”进行安装。根据提示,一步一步进行安装,这里不再啰嗦。最终在会桌面上生成Appium图标,当我双击图标时,那么问题来了。 这个简单,百度“.net framework 4.0” ,百度软件中心提供该框架的下载,将其下载并安装即可。 “.net framework 4.0”安装完成,再次启动Appium,再次弹出提示: 好吧,再次百度“.net framework 4.5”,进行下载安装。再次启动Appium。 好吧!Appium终于可以启动起来了。至于Appium的原理和使用我们放到后面的章节进行介绍。 -------------- 你以为环境就搭建好了么?这才刚开始。
写在前面 在前面一篇文章《【前端模板之路】一、重构的兄弟说:我才不想看你的代码!把HTML给我交出来!》中,我们举了一个人肉各种createElement的例子,那繁琐程度绝对是惨绝人寰。人生本就苦短,每天加班又占据了不少时间,这么折腾下去,还让人怎么活。面对这种场景,我们该怎么做。 无需复杂的构建工具,仅几个简单的工具函数,帮我们告别重复意义的劳动:让代码帮我们写代码! 从最简单的例子说起 让代码帮我们写代码,似乎很豪迈的话,但相信部分童鞋听着还是有些丈二和尚摸不着头脑。那我们暂且抛开这句不知所云的话,来看看下面这个例子。一段简单的HTML <h3>小卡的测试号</h3> 现在让我们来“人肉”创建下这个节点,无非就createElement、createTextNode两个操作 var nick = document.createElement('h3'); // 元素节点 var nickTxt = document.createTextNode('小卡的测试号'); // 文本节点 nick.appendChild(nickTxt); 现在让我们在节点上加多点内容 <h3 class="title">小卡的测试号</h3> 继续我们的人肉操作,与上文类似,只是多了个setAttribute的步骤 var nick = document.createElement('h3'); // 元素节点 nick.setAttribute('class', 'title'); // 设置节点属性 var nickTxt = document.createTextNode('小卡的测试号'); // 文本节点 nick.appendChild(nickTxt); 很简单的例子,到这里为止。可能你有这样的疑惑:这样的例子跟我们的“让代码帮我们写代码”有什么关系。是的,一切的谜底就在其中,请往下看。 创建节点三部曲——你究竟看到了什么 从上面的代码,我们可以看出,人肉创建一个节点——我们用节点P来表示,包含以下三个步骤: 1. 创建节点P 2. 给节点P设置属性 3. 创建节点P的子节点C 其中,步骤3 创建子节点,跟创建一个节点P的过程完全一致,也就是说,这里的关键,是dom树的遍历过程。 那么,我们将要做什么 上面我们已经简单分析了一个节点创建的几个逻辑步骤,那么,现在说下,“让代码帮我们写代码”究竟是什么意思。很简单,那就是:随便给一段HTML文本,自动生成上面那堆createElement、createTextNode、setAttribute 整体目标已经明确,现在我们来分解下子任务: 1. 节点创建(createElement...)自动化 2. 属性设置(setAttribute...)自动化 3. 代码自动格式化 一点必要的准备工作 上面我们提到,代码写代码,实现的关键点在于dom树的遍历。现在我们手头上只有一段HTML文本(字符串),如何遍历?正则什么的有点高端不敢碰,来点奇淫技巧,先把文本转成dom节点,现在,我们的HTML文本就转成可遍历的节点了,即wrapper.childeNodes var wrapper = document.createElement('div'); wrapper.innerHTML = html; var childNodes = wrapper.childNodes; // 我们真正要遍历的节点 目标一:节点创建自动化 废话不多说,直接上代码,逻辑很简单,关键是区分三种不同的节点类型即可。实际上节点类型不止三种,但动态创建过程中常见的也就Element、TextNode两种,如果有需要,可自行补充 function createNode(childNode){ var arr = [], childNodeName = getName( childNode ); // 一个工具方法,返回一个变量名,实现细节先不管它 switch(childNode.nodeType){ case 3: // 文本节点 arr = arr.concat( 'var ' + childNodeName + ' = ' + 'document.createTextNode("'+ childNode.nodeValue +'")' ); break; case 8: // 注释 arr = arr.concat( 'var ' + childNodeName + ' = ' + 'document.createComment("'+ childNode.nodeValue +'")' ); break; default: // 其他 arr.push( 'var '+ childNodeName + ' = ' + 'document.createElement("'+ childNode.nodeName.toLowerCase() +'")' ); break; } return arr; } 目标二:属性设置自动化 直接上代码,我们知道,节点的属性存在一个叫做attributes的特性里,attributes是个NamedNodeMap,名字很奇怪,知道下面几点即可: 1. attributes里存的是节点的属性,举例来说,上面class="title",这个class就是节点的属性 2. attributes是个类数组,可遍历,有个length属性,表示节点属性的个数 3. 每个attributes元素是个对象,该对象有两个关键的属性,即name(节点属性名)和value(节点属性值),如下面代码所示 于是我们得到如下代码 function createAttribute(childNode, childNodeName, tabNum){ var attributes = childNode.attributes, arr = [], childNodeName = getName( childNode ); for(var j=0; j<attributes.length; j++){ var attribute = attributes[j]; arr.push( childNodeName +'.setAttribute("' + attribute.name + '", "' + attribute.value + '");' ); } return arr; } 之前在jQuery源码分析系列里写了篇文章《jQuery源码-jQuery.fn.attr与jQuery.fn.prop》,看了你就会知道,上面这段代码其实是有坑的,但是先不引入额外的复杂度,有时间我再补充(程序员最大的谎言:TODO) 目标三:代码自动格式化 代码多了,一堆createElement、appendChild神马的,一下就把人看晕了,完全看不出层级结构,这个时候加上合理的缩进是很有必要的,缩进的数目跟dom树的深度成正比,直接看个例子 var div_1 = document.createElement("div") div_1.setAttribute("nick", "casepr"); var h1_1 = document.createElement("h1") h1_1.setAttribute("class", "title"); div_1.appendChild( h1_1 ) var text_1 = document.createTextNode("标题") h1_1.appendChild( text_1 ) 这里只贴个简单的工具方法,比如repeat('a', 3)返回 'aaa' // 返回num个str拼成的字符串 function repeat(str, num){ return new Array(num+1).join(str); } 终极奥义——完整的代码实现 简单把代码封装了下,需要关注的是Util.getCode方法,举个例子Util.getCode('<h3 class="title">小卡的测试号</h3>'),看看输出是什么 :) 代码注释写得算是比较详细了,不缀述~~ var Util = (function(){ var map = {}; //console.log( arr.join('\n') ); /** * 核心方法,遍历一个节点,返回创建这个节点需要的完整步骤 * * @param {HTMLElement} parentNode dom节点 * @param {Boolean} needCreateParentNode true: 需要添加parentNode本身的创建步骤;false:不需要 * @param {Number} tabNum tab缩进的数目 * @param {String} parentNodeName 我们已经为parentNode生成的变量名,如无,则为空字符串 * @return {Array} 创建parentNode所需要的完整步骤 */ function getCodeRecursively(parentNode, needCreateParentNode, tabNum, parentNodeName){ var childNodes = parentNode.childNodes, i =0, len = childNodes.length, arr = []; parentNodeName = parentNodeName || getName(parentNode); if( needCreateParentNode ){ arr = arr.concat( createNode(parentNode, parentNodeName, tabNum) ); // 1、create父节点,给父节点setAttribute } ++tabNum; for(; i<len; i++){ var childNode = childNodes[i]; if( shouldTravel(childNode) ){ var childNodeName = getName(childNode); arr = arr.concat( createNode(childNode, childNodeName, tabNum) ); arr.push( repeat('\t', tabNum) + parentNodeName +'.appendChild( '+ childNodeName +' )' ); // 3、塞子节点 arr = arr.concat( getCodeRecursively( childNode, false, tabNum, childNodeName ) ); } } return arr; } /** * 创建属性 * @param {HTMLElement} node 节点 * @param {String} variName 为node起的变量名 * @param {Number} tabNum 缩进数目 * @return {Array} 详细步骤 */ function createAttribute(node, variName, tabNum){ var attributes = node.attributes, arr = []; for(var j=0; j<attributes.length; j++){ var attribute = attributes[j]; arr.push( repeat('\t', tabNum) + variName +'.setAttribute("' + attribute.name + '", "' + attribute.value + '");' ); } return arr; } /** * 创建节点 * @param {HTMLElement} node 节点 * @param {String} variName 为node起的变量名 * @param {Number} tabNum 缩进数目 * @return {Array} 详细步骤 */ function createNode(node, variName, tabNum){ var arr = []; switch(node.nodeType){ case 3: // 文本节点 arr = arr.concat( repeat('\t', tabNum) + 'var ' + variName + ' = ' + 'document.createTextNode("'+ node.nodeValue +'")' ); break; case 8: // 注释 arr = arr.concat( repeat('\t', tabNum) + 'var ' + variName + ' = ' + 'document.createComment("'+ node.nodeValue +'")' ); break; default: // 其他 arr.push( repeat('\t', tabNum) + 'var '+ variName + ' = ' + 'document.createElement("'+ node.nodeName.toLowerCase() +'")' ); arr = arr.concat( createAttribute(node, variName, tabNum) ); break; } return arr; } /** * 是否应该遍历节点(这个方法是否恰当??) * @param {HTMLElement} node 节点 * @return {Boolean} true:应该遍历;false:不应该遍历 */ function shouldTravel( node ){ return node.nodeType==1 || node.nodeValue.trim()!=''; } /** * 返回一个变量名, * @param {HTMLElement} node * @return {String} 变量名,格式为 nodeName_XXX,其中nodeName是节点名的小写,XX为数字,例: div_1 */ function getName(node){ var nodeName = node.nodeName.toLowerCase().replace('#', ''); if(!map[nodeName]){ map[nodeName] = 1; }else{ map[nodeName]++; } return nodeName+ '_' +map[nodeName]; } /** * 返回num个str拼成的字符串 * @param {String} str 一段字符 * @param {Number} num 重复次数 * @return {String} num个str拼成的字符串 */ function repeat(str, num){ return new Array(num+1).join(str); } return { /** * 根据html字符串,返回这段字符串对应的dom节点的完整创建过程 * @param {String} html HTML字符串 * @return {Array} 创建步骤 */ getCode: function(html){ var arr = [], // map = {}, i = 0, len = 0, childNodes = []; map = {}; var wrapper = document.createElement('div'); wrapper.innerHTML = html; childNodes = wrapper.childNodes; // 这段代码也是可以提取的,TODO吧 len = childNodes.length; for(; i<len; i++){ var childNode = childNodes[i]; if(shouldTravel(childNode)){ arr = arr.concat( getCodeRecursively(childNode, true, 0, '') ); } } return arr; } }; })(); 你让我肿么相信你——测试用例 附上简短测试用例一枚: var html = '<div nick="casepr">\ <h1 class="title">标题</h1>\ 纯文本节点\ <!--注释-->\ <div class="content">\ <div class="preview">预览</div>\ <div class="content">正文</div>\ </div>\ <label for="box" class="select">选择:</label>\ <input type="checkbox" id="box" name="box" checked="checked" />\ </div>'; console.log( Util.getCode(html).join('\n') ); 输出结果: var div_1 = document.createElement("div") div_1.setAttribute("nick", "casepr"); var h1_1 = document.createElement("h1") h1_1.setAttribute("class", "title"); div_1.appendChild( h1_1 ) var text_1 = document.createTextNode("标题") h1_1.appendChild( text_1 ) var text_2 = document.createTextNode(" 纯文本节点 ") div_1.appendChild( text_2 ) var comment_1 = document.createComment("注释") div_1.appendChild( comment_1 ) var div_2 = document.createElement("div") div_2.setAttribute("class", "content"); div_1.appendChild( div_2 ) var div_3 = document.createElement("div") div_3.setAttribute("class", "preview"); div_2.appendChild( div_3 ) var text_3 = document.createTextNode("预览") div_3.appendChild( text_3 ) var div_4 = document.createElement("div") div_4.setAttribute("class", "content"); div_2.appendChild( div_4 ) var text_4 = document.createTextNode("正文") div_4.appendChild( text_4 ) var label_1 = document.createElement("label") label_1.setAttribute("for", "box"); label_1.setAttribute("class", "select"); div_1.appendChild( label_1 ) var text_5 = document.createTextNode("选择:") label_1.appendChild( text_5 ) var input_1 = document.createElement("input") input_1.setAttribute("type", "checkbox"); input_1.setAttribute("id", "box"); input_1.setAttribute("name", "box"); input_1.setAttribute("checked", "checked"); div_1.appendChild( input_1 ) 写在后面 罗里八嗦地写了这么多,终于实现了本文最前面提到的“让代码帮我们写代码”这个目的,实现原理很简单,代码也不复杂,不过真正调试的时候还是花了点时间。时间精力所限,代码难免有疏漏之处(不是无聊的谦词,比如“属性设置自动化”那里的坑还没填。。。),如发现,请指出!!!!!!!
bootstrap 的学习非常简单,并且它所提供的样式又非常精美。只要稍微简单的学习就可以制作出漂亮的页面。 bootstrap中文网:http://v3.bootcss.com/ bootstrap提供了三种类型的下载: ------------------------------------------------------------- 用于生产环境的 Bootstrap 编译并压缩后的 CSS、JavaScript 和字体文件。不包含文档和源码文件。 Bootstrap 源码 Less、JavaScript 和 字体文件的源码,并且带有文档。需要 Less 编译器和一些设置工作。 Sass 这是 Bootstrap 从 Less 到 Sass 的源码移植项目,用于快速地在 Rails、Compass 或 只针对 Sass 的项目中引入。 ------------------------------------------------------------ 其实我们不用下载bootstrap也可以使用它: Bootstrap 中文网 为 Bootstrap 专门构建了自己的免费 CDN 加速服务。基于国内云厂商的 CDN 服务,访问速度更快、加速效果更明显、没有速度和带宽限制、永久免费。 base.html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --> <title>Bootstrap 101 Template</title> <!-- Bootstrap --> <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap.min.css"> </head> <body> <h1>你好,bootstrap!</h1> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="http://cdn.bootcss.com/jquery/1.11.2/jquery.min.js"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="http://cdn.bootcss.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> </body> </html> base.html中已经引入了bootstrap,将其保存,我们就可以使用bootstrap提供的样式了。 字体图标 bootstrap默认提供了二百多个图标。我们可以通过span标签来使用这些图标: <h3>图标</h3> <span class="glyphicon glyphicon-home"></span> <span class="glyphicon glyphicon-signal"></span> <span class="glyphicon glyphicon-cog"></span> <span class="glyphicon glyphicon-apple"></span> <span class="glyphicon glyphicon-trash"></span> <span class="glyphicon glyphicon-play-circle"></span> <span class="glyphicon glyphicon-headphones"></span> 按钮 <button></button>标签用于创建按钮,bootstrap提供了丰富的按钮样式。 <h3>按钮</h3> <button type="button" class="btn btn-default">按钮</button> <button type="button" class="btn btn-primary">primary</button> <button type="button" class="btn btn-success">success</button> <button type="button" class="btn btn-info">info</button> <button type="button" class="btn btn-warning">warning</button> <button type="button" class="btn btn-danger">danger</button> <h3>按钮尺寸</h3> <button type="button" class="btn btn-default">按钮</button> <button type="button" class="btn btn-primary btn-lg">primary</button> <button type="button" class="btn btn-success btn-sm">success</button> <button type="button" class="btn btn-info btn-xs">info</button> <h3>把图标显示在按钮里</h3> <button type="button" class="btn btn-default"><span class="glyphicon glyphicon-home"></span>&nbsp;&nbsp;按钮</button> 按钮除了有默认的大小外,bootstrap还提供三个参数来调整按钮的大小,分别是:btn-lg、btn-sm和btn-xs。 下拉菜单 下拉菜单是最常见的交互之一,bootstrap提供了漂亮的样式。 <h3>下拉菜单</h3> <div class="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenu1" data-toggle="dropdown" aria-expanded="true"> Dropdown <span class="caret"></span> </button> <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1"> <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Action</a></li> <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Another action</a></li> <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Something else here</a></li> <li role="presentation"><a role="menuitem" tabindex="-1" href="#">Separated link</a></li> </ul> </div> 输入框 通过<input></input>标签去创建输入框。 <h3>输入框</h3> <div class="input-group"> <span class="glyphicon glyphicon-user"></span> <input type="text" placeholder="username"> </div> <div class="input-group"> <span class="glyphicon glyphicon-lock"></span> <input type="password" placeholder="password"> </div> 导航栏 导航栏作为整个网站的指引必不可少。 <h3>导航栏</h3> <nav class="navbar navbar-inverse navbar-fixed-top"> <div id="navbar" class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Home</a></li> <li><a href="#about">About</a></li> <li><a href="#contact">Contact</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Dropdown <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="#">Action</a></li> <li><a href="#">Another action</a></li> <li class="divider"></li> <li class="dropdown-header">Nav header</li> <li><a href="#">Separated link</a></li> </ul> </li> </ul> </div><!--/.nav-collapse --> </div> </nav> 表单 人与系统之间数据的传递都需要依靠表单来完成。比如注册/登录信息的提交,查询条件的提交等。用<form></form>标签来创建表单。 <h3>表单</h3> <form> <div class="form-group"> <span class="glyphicon glyphicon-user"></span> <input type="email" id="exampleInputEmail1" placeholder="Enter email"> </div> <div class="form-group"> <span class="glyphicon glyphicon-lock"></span> <input type="password" id="exampleInputPassword1" placeholder="Password"> </div> <div class="form-group"> <label for="exampleInputFile">File input</label> <input type="file" id="exampleInputFile"> <p class="help-block">Example block-level help text here.</p> </div> <div class="checkbox"> <label> <input type="checkbox"> Check me out </label> </div> <button type="submit" class="btn btn-default">Submit</button> </form> 警告框 警告框是系统向用户传达信息和提供指引的重要手段。没有针对警告框的标签,通过bootstrap所提供的样式可以瞬间制作出漂亮的警告框。 <h3>警告框</h3> <div class="alert alert-warning alert-dismissible" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button> <strong>Warning!</strong> Better check yourself, you're not looking too good. </div> <div class="alert alert-success" role="alert"> <a href="#" class="alert-link">success!</a> </div> <div class="alert alert-info" role="alert"> <a href="#" class="alert-link">info!</a> </div> <div class="alert alert-warning" role="alert"> <a href="#" class="alert-link">warning!</a> </div> <div class="alert alert-danger" role="alert"> <a href="#" class="alert-link">danger!</a> </div> 进度条 系统的处理过程往往需要用户等待,进度条可以让用户感知到系统的处理过程,从而增加容忍度。 <h3>进度条</h3> <div class="progress"> <div class="progress-bar" role="progressbar" aria-valuenow="70" aria-valuemin="0" aria-valuemax="100" style="width: 60%;"> 70% </div> </div> ===========结束=========== 1、我觉得前端更像是艺术,人们对美好的东西从来不会产生分歧。前端更像是通过技术展示美好。 2、前端技术的学习是所见即所得,你可以任意的修改标签及其属性,并且立马看到修改后的效果。 3、对于bootstrap来说,标签最重要的属性就是class,因为使用的不同的class属性值,可以使你的标签样式颜色发生变化。 4、这篇文章很简单,只是罗列一些最基本的页面元素组成。bootstrap更多的学习:http://v3.bootcss.com/
如果你看过我之前所写的关于django的文章的话,你会发现每一篇都具有可操作性,都是从创建项目开始的,虽然中间之加了一些要讲解的重点。这也是我博文的特点,我希望在你看到我这一篇文章的时候是可操作的,不管是否具备了相关基础。 如果你是第一次接触django,建议参考我的之关于django的内容练习一下: http://www.cnblogs.com/fnng/category/581256.html 这一篇要介绍django是如何工作的。如果你把这过程梳理清晰了,那么你对django就算入门了。 一张流程图告诉你,django的处理流程: URL组成 作为网站的用户,我们首先在浏览器的输入框内输入:http://127.0.0.1:8000/index/ URL地址由以下几部分组成: 协议类型: HTTP ,FTP HTTP协议(HyperText Transfer Protocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还确定传输文档中的哪一部分,以及哪部分内容首先显示等 。 HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。 主机地址:itest.info ,127.0.0.1 前者是一个网址,网址通过域名解析服务器会找到对应的IP地址。后者就是一个IP地址。 127.0.0.1 指向的就是本机的IP地址。 端口号: 8000 端口号用于区分标识同一台主机的不同应用。一台主机上有很多应用,你访问我这台主机时如果指定的是8000,那么就是知道是你来访问我用django开发的blog的。当然,这个端口是可以任意分配的。 路径 : /index/ 、/admin 一般用来表示主机上的一个目录或文件地址。 urls的配置 django通过urls.py配置文件很好的处理了前端请求的指向,其中使用使用Python的正则表达式可以使匹配变得更灵活。 打开django下面的urls.py文件: from django.conf.urls import patterns, include, url from django.contrib import admin urlpatterns = patterns('', url(r'^admin/', include(admin.site.urls)), url(r'^index/$', 'blog.views.index'), ) r'^index/$' 这是一个使用了python的正则表达式。 字符串有前面加“ r ”是为了防止字符串中出现类似“\t”字符时被转义。 django在拿到URL地址后,取端口号后面的文件夹路径(/index/)进行配置,结果^index/$ 可以对这个文件路径进行匹配。那么将指向blog.views.index 这个地址。 blog是文件夹,views是一个.py文件,index 则是函数名。 model模型 Django用模型在后台执行SQL代码并把结果用Python的数据结构来描述。Django也使用模型来呈现SQL无法处理的高级概念。 模型用于数据库的创建,在settings.py文件中配置数据库的连接,例如一个mysql数据库的配置: …… DATABASES = { 'default': { 'ENGINE' : 'django.db.backends.mysql', 'HOST' : '127.0.0.1', 'PORT' : '3306', 'NAME' : 'myweb', 'USER' : 'user', 'PASSWORD' : '123456', } } …… 配置信息从上到下依次是驱动(ENGINE),主机地址(HOST),端口号(PORT),数据库(NAME),登录用户名(USER),登录密码(PASSWORD)。 在应用的的models.py文件中创建模型,为了避免直接操作数据库,通过创建模型去生成对应的数据库表。 from django.db import models # Create your models here. class BlogsPost(models.Model): title = models.CharField(max_length = 150) body = models.TextField() timestamp = models.DateTimeField() 执行数据库同步会创建一张PlogsPost表,表分别会有title、body、timestamp三个字段。其中title定义为char类型,定义最长150字符;body为text文本类型;timestamp为日期时间类型。 我们不用关心到底怎么创建表,只要创建好模型就好了,剩下的由djnago来帮我们生成对应的表。下面是将模型创建成数据库表的命令: D:\pydj\myweb>python manage.py makemigrations blog Migrations for 'blog': 0001_initial.py: - Create model BlogsPost D:\pydj\myweb>python manage.py syncdb Operations to perform: Apply all migrations: admin, blog, contenttypes, auth, sessions Running migrations: Applying blog.0001_initial... OK 首先是通过 makemigrations对某个应用(blog)下的模型执行迁移;然后通过syncdb检测项目下的所有模型,发现模型有变动或未生成表,将重新生成相应的表。(在djnaog 1.7之前的版本只需要syncdb一个命令就可以完成同步) views视图 视图可以看作是前端与数据库的中间人,他会将前端想要的数据从数据库中读出来给前端。他也会将用户要想保存的数据写到数据库。 views.py #coding=utf-8 from django.shortcuts import render from blog.models import BlogsPost from django.shortcuts import render_to_response # Create your views here. def index(request): blog_list = BlogsPost.objects.all() return render_to_response('index.html',{'blog_list':blog_list}) 这里index函数做了两件事儿: blog_list =BlogsPost.objects.all() 查询到BlogsPost数据库里的所有数据,赋值给blog_list变量。 return render_to_response('index.html',{'blog_list':blog_list}) 通过render_to_response() 返回给浏览器一个index.html页面,并且将blog_list变量的值也返回给index.html。 templates模板 模板就是我们所熟悉的页面了,django自带的有模板系统。它的主要作用是如何展示数据,比如视图返回了一堆数据过来。是都循环显示出来呢?还通过判断只显示你认为有用的呢? 当然,这里为了使页面更漂亮需要借助前端技术,比如css、JavaScript等。 然后,我们就在浏览器上看到了index.html页面了: MTV开发模式 了解了django的组成部分之间,我们再来深入的探讨一下django的开发模式。 MTV 开发模式 在钻研更多代码之前,让我们先花点时间考虑下 Django 数据驱动 Web 应用的总体设计。 我们在前面章节提到过,Django 的设计鼓励松耦合及对应用程序中不同部分的严格分割。 遵循这个理念的话,要想修改应用的某部分而不影响其它部分就比较容易了。 在视图函数中,我们已经讨论了通过模板系统把业务逻辑和表现逻辑分隔开的重要性。 在数据库层中,我们对数据访问逻辑也应用了同样的理念。 把数据存取逻辑、业务逻辑和表现逻辑组合在一起的概念有时被称为软件架构的 Model-View-Controller(MVC)模式。 在这个模式中, Model 代表数据存取层,View 代表的是系统中选择显示什么和怎么显示的部分,Controller 指的是系统中根据用户输入并视需要访问模型,以决定使用哪个视图的那部分。 为什么用缩写? 像 MVC 这样的明确定义模式的主要用于改善开发人员之间的沟通。 比起告诉同事,“让我们采用抽象的数据存取方式,然后单独划分一层来显示数据,并且在中间加上一个控制它的层”,一个通用的说法会让你收益,你只需要说:“我们在这里使用MVC模式吧。”。 Django 紧紧地遵循这种 MVC 模式,可以称得上是一种 MVC 框架。 以下是 Django 中 M、V 和 C 各自的含义: M ,数据存取部分,由django数据库层处理,本章要讲述的内容。 V ,选择显示哪些数据要显示以及怎样显示的部分,由视图和模板处理。 C ,根据用户输入委派视图的部分,由 Django 框架根据 URLconf 设置,对给定 URL 调用适当的 Python 函数。 由于 C 由框架自行处理,而 Django 里更关注的是模型(Model)、模板(Template)和视图(Views), Django 也被称为 MTV 框架 。在 MTV 开发模式中: M 代表模型(Model),即数据存取层。 该层处理与数据相关的所有事务: 如何存取、如何验证有效 T 代表模板(Template),即表现层。 该层处理与表现相关的决定: 如何在页面或其他类型文档中进行显示。 V 代表视图(View),即业务逻辑层。 该层包含存取模型及调取恰当模板的相关逻辑。 你可以把它看作模型与模板之间的桥梁。 如果你熟悉其它的 MVC Web开发框架,比方说 Ruby on Rails,你可能会认为 Django 视图是控制器,而Django 模板是视图。 很不幸,这是对 MVC 不同诠释所引起的错误认识。 在 Django 对 MVC 的诠释中,视图用来描述要展现给用户的数据;不是数据 如何展现 ,而且展现 哪些 数据。 相比之下,Ruby on Rails 及一些同类框架提倡控制器负责决定向用户展现哪些数据,而视图则仅决定 如何展现数据,而不是展现 哪些 数据。 两种诠释中没有哪个更加正确一些。 重要的是要理解底层概念。
当我第一次使用Robot Framework时,我是拒绝的。我跟老大说,我拒绝其实对于习惯了代码的自由,所以讨厌这种“填表格”式的脚本。老大说,Robot Framework使用简单,类库丰富,还可以自由开发系统关键字。那我说,你不能让我用我就用,我要先用用看。自从我用了半年多以来,duang~! 真的挺好用的。duang~! ,我相信我用完是这个样子,你们用完也是这个样子。duang~! duang~! ----今年流行“duang”,我也来一段。哈哈~! Robot Framework特点: l 使用简单 l 非常丰富的库 l 可以像编程一样写测试用例 l 支持开发系统关键字 上面几点是我使用过程的体会。当然,Robot Framework的特点还有其它。 1、使用简单。当你真的要向项目中推广一个技术或工具的时候,其实这点非常重要。对于大多测试团队的测试人员来说,开发技术还是很薄弱的。Robot Framework使用非常简单,只要告诉你是这些关键字是做什么用的,你去“填表格”就好的。 2、非常丰富的类库,支持Robot Framework的库很多,标准库加扩展库有几十个。 web自动化测试:SeleniumLibrary,Selenium2Library,Selenium2Library for Java、watir-robot等。 Windows GUI测试:AutoItLibrary。 移动测试:Android library、iOS library、AppiumLibrary等。 数据库测试:Database Library (Java)、Database Library (Python)、MongoDB library等。 文件对比测试:Diff Library。 HTTP测试:HTTP library (livetest)、HTTP library (Requests)等。 3、Robot Framework 可不是只能写一些死板的操作过程,定义变量,数组、字典,写if判断,for循环都不在话下,甚至调用python所提供的方法;你懂pyhon,可以把它玩得游刃有余。 4、开发系统关键字,或者自己写个自定义库也很简单,用工具,但又不会受制于人工具。这也是我用它的一点。当然,前提还是你会点python。
最近一直在用robot framework 做自动化测试项目,老实说对于习惯直接使用python的情况下,被框在这个工具里各种不爽,当然,使用工具的好处也很多,降低了使用成本与难度;当然,在享受工具带来便利的同时也会受制于工具。对于特定的需求,工具没提供相关的Library和关键字的时候,就只能放弃了。 还好robot framework提供了 Evaluate 关键字,对于Evaluate 关键字的使用等有时间再讲。当robot framework 不能解决需求,我是直接写个.py 程序,通过Evaluate 关键字调用。然后,就受到了批评,不能这么玩,动不动就这么干的话其实robot framework 就成了鸡肋,所以,规范的做法是去封装系统关键字。 这也是本文的目的,学会了这一招之后,robot framework 就算是玩转了,当然,前提是你要懂点Python才行。 其实我的需求也非常简单,接收一个目录路径,自动遍历目录下以及子目录下的所有批处理(.bat)文件并执行。 首先在..\Python27\Lib\site-packages目录下创建CustomLibrary目录,用于放自定义的library库。在其下面创建runbat.py 文件: #-*- coding:utf-8 -*- ''' created by bugmaster 2015-01-29 ''' __version__ = '0.1' from robot.api import logger import os class Runbat(object): def run_all_bat(self,path): u'''接收一个目录的路径,并执行目录下的所有bat文件.例 | run all bat | filepath | ''' for root,dirs,files in os.walk(path): for f in files: if os.path.splitext(f)[1] == '.bat': os.chdir(root) #print root,f os.system(f) def __execute_sql(self, path): logger.debug("Executing : %s" % path) print path def decode(self,customerstr): return customerstr.decode('utf-8') if __name__ == "__main__": path = u'D:\\test_boject' run = Runbat() run.run_all_bat(path) 注意在run_all_bat()方法下面加上清晰的注释,最好给个实例。这样在robot framework 的帮助中能看到这些信息,便于使用者理解这个关键字的使用。 对于创建普通的模块来说这样已经ok了。但要想在robot framework启动后加载这个关键字,还需要在CustomLibrary目录下创建__init__.py文件,并且它不是空的。 # Copyright (c) 2010 Franz Allan Valencia See # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from runbat import Runbat __version__ = '0.1' class CustomLibrary(Runbat): """ 这里也可以装x的写上我们创建的CustomLibrary如何如何。 """ ROBOT_LIBRARY_SCOPE = 'GLOBAL' 这个文件中其实有用的信息就三行,但必不可少。robot framwork 在启动时会加载这个文件,因为在这个文件里指明了有个runbat文件下面有个Runbat类。从而加载类里的方法(run_all_bat())。 下面,启动robot framework RIDE,按F5: 找到了我们创建的关键字,下面就是在具体的项目或测试套件中引用CustomLibrary 然后,在具体的测试用例中使用“run all bat” 关键字。 其实核心还是会点Python ,利用工具,但又不受制于工具。 ==================================== 前几天有个同学跑来给我发了个链接,是一个新的自动化测试工具, 然后告诉我:“你看,这工具多牛B ,能自动录制,不用写一行代码。那你说学pyhon 还有毛用”。测试工具早前面对的就是一群不会编程的人好吧。录制也早不是什么新鲜的技术了好吧。都能录制了,想想我们是不是早该下岗了。很多时候录制并不是万能,所以才有高级测试人才的生存与发展空间。如果有一天你只会录制,想想离下岗也不远了。因为新招来的任劳任怨还不嫌工资低。 -------新年快乐,明年再见。
AutoIt目前最新是v3版本,这是一个使用类似BASIC脚本语言的免费软件,它设计用于Windows GUI(图形用户界面)中进行自动化操作。它利用模拟键盘按键,鼠标移动和窗口/控件的组合来实现自动化任务。 官方网站:https://www.autoitscript.com/site/ 从网站上下载AutoIt并安装,安装完成在菜单中会看到图4.13的目录: 图4.13 AutoIt菜单 AutoIt Windows Info 用于帮助我们识Windows控件信息。 Compile Script to.exe 用于将AutoIt生成 exe 执行文件。 Run Script 用于执行AutoIt脚本。 SciTE Script Editor 用于编写AutoIt脚本。 <html> <head> <meta http-equiv="content-type" content="text/html;charset=utf-8" /> <title>upload_file</title> <link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" /> </head> <body> <div class="row-fluid"> <div class="span6 well"> <h3>upload_file</h3> <input type="file" name="file" /> </div> </div> </body> <script src="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.js"></script> </html> 将上面的html代码保存为uplad.html文件,通过浏览器打开,效果如下: 下面以操作upload.html上传弹出的窗口为例讲解AutoIt实现上传过程。 1、首先打开AutoIt Windows Info 工具,鼠标点击Finder Tool,鼠标将变成一个小风扇形状的图标,按住鼠标左键拖动到需要识别的控件上。 图4.14 AutoIt Windows Info识别“文件名”输入框控件 图4.15 AutoIt Windows Info识别“打开”按钮控件 如图4.14、4.15,通过AutoIt Windows Info 获得以下信息。 窗口的title为“选择要加载的文件”,标题的Class为“#32770”。 文件名输入框的class 为“Edit”,Instance为“1” ,所以ClassnameNN为“Edit1”。 打开按钮的class 为“Button”,Instance为“1” ,所以ClassnameNN为“Button1”。 2、根据AutoIt Windows Info 所识别到的控件信息打开SciTE Script Editor编辑器,编写脚本。 ;ControlFocus("title","text",controlID) Edit1=Edit instance 1 ControlFocus("选择要加载的文件", "","Edit1") ; Wait 10 seconds for the Upload window to appear WinWait("[CLASS:#32770]","",10) ; Set the File name text on the Edit field ControlSetText("选择要加载的文件", "", "Edit1", "D:\\upload_file.txt") Sleep(2000) ; Click on the Open button ControlClick("选择要加载的文件", "","Button1"); ControlFocus()方法用于识别Window窗口。WinWait()设置10秒钟用于等待窗口的显示,其用法与WebDriver 所提供的implicitly_wait()类似。ControlSetText()用于向“文件名”输入框内输入本地文件的路径。这里的Sleep()方法与Python中time模块提供的Sleep()方法用法一样,不过它是以毫秒为单位,Sleep(2000)表示固定休眠2000毫秒。ControlClick()用于点击上传窗口中的“打开”按钮。 AutoIt的脚本已经写好了,可以通过菜单栏“Tools”-->“Go” (或按键盘F5)来运行一个脚本吧!注意在运行时上传窗口当前处于打开状态。 3、脚本运行正常,将其保存为upfile.au3,这里保存的脚本可以通过Run Script 工具将其打开运行,但我们的目的是希望这个脚本被Python程序调用,那么就需要将其生成exe程序。打开Compile Script to.exe工具,将其生成为exe可执行文件。如图4.16, 图4.16 Compile Script to.exe生成exe程序 点击“Browse”选择upfile.au3文件,点击“Convert”按钮将其生成为upfile.exe程序。 4、下面就是通过自动化测试脚本调用upfile.exe程序实现上传了。 #coding=utf-8 from selenium import webdriver import os driver = webdriver.Firefox() #打开上传功能页面 file_path = 'file:///' + os.path.abspath('upfile.html') driver.get(file_path) #点击打开上传窗口 driver.find_element_by_name("file").click() #调用upfile.exe上传程序 os.system("D:\\upfile.exe") driver.quit() 通过Python 的os模块的system()方法可以调用exe程序并执行。 了解了上传的实现过程,那么下载也是一样的。 《selenium2 python 自动化测试实战》 --new
我想这应该是很普遍的一篇文章,百度了一下确实有不少相关的文章,居然还在讲用“mod_python” , 我也是醉了。在些过程中颇费了些力气。在此记录。 ---------------------------------------------- 在此之前,我们一直使用django的manage.py 的runserver 命令来运行django应用,但这只是我们的开发环境,当项目真正部署上线的时候这做就不可行了,必须将我们的项目部署到特定的web服务器上。 安装apache Apache是非常有名的web服务器软件,如果想让我们web项目运行几乎离不开它。 Apache官方网站:http://httpd.apache.org/ 根据自己的环境,选择相应的版本进行下载。apache 官网没有windows 64位版本,可以通过下面的链接进行下载:win7 64位:http://www.apachelounge.com/download/win64/ 下载安装完成,apahche的目录结构如下: 修改conf/httpd.conf文件: …… ServerRoot "D:/pydj/Apache24" …… Listen 127.0.0.1:8089 #修改端口号 …… ServerName www.example.com:8089 …… DocumentRoot "D:/pydj/Apache24/htdocs" <Directory "D:/pydj/Apache24/htdocs"> …… ScriptAlias /cgi-bin/ "D:/pydj/Apache24/cgi-bin/" …… <Directory "D:/pydj/Apache24/cgi-bin"> AllowOverride None Options None Require all granted </Directory> …… 主要就是路径和端口号的修改,如果你在启动apache的httpd.exe程序时一闪就没了,请检查这些配置。 启动bin/httpd.exe程序 通过浏览器访问:http://127.0.0.1:8089/ 现在可以说明apache工作是正常的了。 安装mod_wsgi The aim of mod_wsgi is to implement a simple to use Apache module which can host any Python application which supports the Python WSGI interface. The module would be suitable for use in hosting high performance production web sites, as well as your average self managed personal sites running on web hosting services. (mod_wsgi的目的是实现一个简单的使用Apache模块可以举办任何Python应用程序支持Python的WSGI接口。该模块将适用于主机的高性能生产的网站,以及一般的自我管理个人网站的网页寄存服务运行。)直接google翻译的,凑合的大概理解是干啥用的。 mod_wsgi网站:http://code.google.com/p/modwsgi/ 下载地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/#mod_wsgi 如win7 64位、python 2.7.6、apache(httpd-2.4.10)对应版本为:mod_wsgi-3.5.ap24.win-amd64-py2.7.zip 解压之后将得到一个mod_wsgi.so 文件,将其拷贝到Apache24\modules\ 目录下。 配置apache和django项目 因为你的目录一定和我的一样,所以,我再强调一下我的目录: apache 存放目录:D:\pydj\Apache24 django项目目录:D:\pydj\myweb 再次打apache的配制文件httpd.conf: …… #添加mod_wsgi.so 模块 LoadModule wsgi_module modules/mod_wsgi.so #指定myweb项目的wsgi.py配置文件路径 WSGIScriptAlias / D:/pydj/myweb/myweb/wsgi.py #指定项目路径 WSGIPythonPath D:/pydj/myweb <Directory D:/pydj/myweb/myweb> <Files wsgi.py> Require all granted </Files> </Directory> 上面的路径,请根据自己的实际情况进行修改。 下面配置myweb/wsgi.py文件: …… import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myweb.settings") from django.core.wsgi import get_wsgi_application application = get_wsgi_application() 在我们生成djnago项目时这些信息已经自动生成,其实我们不用对其做任何修改。 打开settings.py文件添加: …… ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] 再次启动Apache24/bin/httpd.exe程序 通过浏览器访问:http://127.0.0.1:8089/ ================================= 备注:最近博客没更新技术,是因为我在整理《django学习手册》,是一本一定可以让你学会开发网站手册,没有废话,没有大道理,跟着做,原来用django开发如些简单。
----对于用户来说,界面就是程序本身。那么一个漂亮的web一定是你继续使用这个应用的前题。 这一节我们来一起写个Bootstrap的hello wrold。 Bootstrap Bootstrap 是最受欢迎的 HTML、CSS 和 JS 框架,用于开发响应式布局、移动设备优先的 WEB 项目。 如何使用Bootstrap? Bootstrap的使用一般有两种方法。一种是引用在线的Bootstrap的样式,一种是将Bootstrap下载到本地进行引用。 引用在线样式: 引用在线样式的好处就是不用本地安装Bootstrap,也是不用考虑引用时的路径问题。缺点是担心性能问题,一旦在线样式挂了,那么自己的网站页面样式也就乱掉了。 http://v3.bootcss.com/getting-started/#download Bootstrap中文网为 Bootstrap 专门构建了自己的免费 CDN 加速服务。 使用方法非常简单: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello Bootstrap</title> <!-- Bootstrap core CSS --> <link href="http://cdn.bootcss.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <h1>hello Bootstrap<h1> </body> </html> <link href="http://cdn.bootcss.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"> 这一行已经将在线的样式引进来了。注意本文使用的是当前最新的Bootstrap3.2.0。 使用本地的Bootstrap 下载Bootstrap到本地进行解压,解压完成,你将得到一个Bootstrap目录,结构如下: bootstrap/ ├── css/ │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── bootstrap-theme.css │ └── bootstrap-theme.min.css ├── js/ │ ├── bootstrap.js │ └── bootstrap.min.js └── fonts/ ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf └── glyphicons-halflings-regular.woff 本地调用如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello Bootstrap</title> <!-- Bootstrap core CSS --> <link href="./bootstrap-3.2.0-dist/css/bootstrap.min.css" rel="stylesheet"> <style type='text/css'> body { background-color: #CCC; } </style> </head> <body> <h1>hello Bootstrap<h1> </body> </html> <link href="./bootstrap-3.2.0-dist/css/bootstrap.min.css" rel="stylesheet"> --表示引入当前目录下的Bootstrap样式。 <link href="D:/bootstrap-3.2.0-dist/css/bootstrap.min.css" rel="stylesheet"> --当然也可以使用绝对路径。 我们多加了一个背景色效果如下: 下面利用Bootstrap的样式编写一个网站出来。 添加导航行栏和登录框 <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">首页</a> <a class="navbar-brand" href="#">测试</a> <a class="navbar-brand" href="#">开发</a> </div> <div id="navbar" class="navbar-collapse collapse"> <form class="navbar-form navbar-right" role="form"> <div class="form-group"> <input type="text" placeholder="Email" class="form-control"> </div> <div class="form-group"> <input type="password" placeholder="Password" class="form-control"> </div> <button type="submit" class="btn btn-success">Sign in</button> </form> </div><!--/.navbar-collapse --> </div> </nav> 浏览器效果如下: 添加一篇文章 <div class="jumbotron"> <div id='content' class='row-fluid'> <h2>Hello, world!</h2> <p class="blog-post-meta">January 1, 2014 by <a href="#">Mark</a></p> <p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p> <p><a class="btn btn-primary btn-lg" role="button">阅读全文 &raquo;</a></p> </div> </div> 浏览器效果如下: 添加底部介绍与友情链接 <div class="col-sm-3 col-sm-offset-1 blog-sidebar"> <div class="sidebar-module sidebar-module-inset"> <h4>About</h4> <p>Etiam porta <em>sem malesuada magna</em> mollis euismod. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.</p> </div> <div class="sidebar-module"> <h4>Elsewhere</h4> <ol class="list-unstyled"> <li><a href="#">博客园</a></li> <li><a href="#">开源中国</a></li> <li><a href="#">infoq</a></li> </ol> </div> </div> 最终效果如下: 完整代码: View Code 样式的继承 你一定很好奇,这些样式是怎么玩的?如何你细心的就会留意到div 标签的class属性。 通过class的属性值去继承Bootstrap的样式定义,那么就达到了某种样式效果。 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>自定义样式</title> <!--自定义侧边栏样式--> <style> .divcss5-right{width:320px; height:120px;border:1px solid #F00;float:right} </style> </head> <body> <!--class属性引用自定义样式--> <div class="divcss5-right"> <h4>友情链接:</h4> <ol class="list-unstyled"> <li><a href="#">博客园</a></li> <li><a href="#">开源中国</a></li> <li><a href="#">infoq</a></li> </ol> </div> </body> </html> 玩前端就是要不断的修改里面的属性或信息,然后看浏览器上的展示效果。
继续django学习之旅,之前我们所做的Django练习前端都非常丑。这节我们使用Bootstrap,顿时使丑陋的页面变成白天鹅。 安装Bootstrap Bootstrap是什么? Bootstrap是Twitter推出的一个用于前端开发的开源工具包。它由Twitter的设计师Mark Otto和Jacob Thornton合作开发,是一个CSS/HTML框架。Bootstrap提供了优雅的HTML和CSS规范,它即是由动态CSS语言Less写成。 django-bootstrap-toolkit django-bootstrap-toolkit应用可以让Django非容易的集成Bootstrap。 安装django-bootstrap-toolkit >pip install django-bootstrap-toolkit 运行bootstrap例子 克隆django-bootstrap-toolkit 项目 https://github.com/dyve/django-bootstrap-toolkit $ git clone git://github.com/dyve/django-bootstrap-toolkit.git 克隆下来的django-bootstrap-toolkit 项目自带demo_project,现在我们可以直接运行这个demo了。 进入demo_project 目录运行: > python manage.py runserver 通过浏览器访问:http://127.0.0.1:8000/ wa o !! cool 比我们之前的djngo例子好看多了。 预览demo_project 来看一下这个项目的结构吧! 通过前面多个django项目练习,我们已经对这个目录结构不陌生了。下面看看这个例子要特别注意的: settings.py …… INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', # Uncomment the next line to enable the admin: # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', 'bootstrap_toolkit', 'demo_app', ) …… 要想使用bootstrap,这里必须加载bootstrap_toolkit ,demo_app则是我们当前的项目。 urls.py from django.conf.urls import patterns, url # Uncomment the next two lines to enable the admin: # from django.contrib import admin # admin.autodiscover() from django.views.generic import TemplateView urlpatterns = patterns('', # Examples: # url(r'^$', 'demo_project.views.home', name='home'), # url(r'^demo_project/', include('demo_project.foo.urls')), # Uncomment the admin/doc line below to enable admin documentation: # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: # url(r'^admin/', include(admin.site.urls)), url(r'^$', TemplateView.as_view(template_name='index.html'), name="home"), url(r'^contact$', TemplateView.as_view(template_name='contact.html'), name="contact"), url(r'^form$', 'demo_app.views.demo_form'), url(r'^form_template$', 'demo_app.views.demo_form_with_template'), url(r'^form_inline$', 'demo_app.views.demo_form_inline'), url(r'^formset$', 'demo_app.views.demo_formset', {}, "formset"), url(r'^tabs$', 'demo_app.views.demo_tabs', {}, "tabs"), url(r'^pagination$', 'demo_app.views.demo_pagination', {}, "pagination"), url(r'^widgets$', 'demo_app.views.demo_widgets', {}, "widgets"), url(r'^buttons$', TemplateView.as_view(template_name='buttons.html'), name="buttons"), ) 下面再看看views.py写了哪些中间逻辑: from django.contrib import messages from django.forms.formsets import formset_factory from django.shortcuts import render_to_response from django.template.context import RequestContext from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from bootstrap_toolkit.widgets import BootstrapUneditableInput from .forms import TestForm, TestModelForm, TestInlineForm, WidgetsForm, FormSetInlineForm def demo_form_with_template(request): layout = request.GET.get('layout') if not layout: layout = 'vertical' if request.method == 'POST': form = TestForm(request.POST) form.is_valid() else: form = TestForm() modelform = TestModelForm() return render_to_response('form_using_template.html', RequestContext(request, { 'form': form, 'layout': layout, })) def demo_form(request): messages.success(request, 'I am a success message.') layout = request.GET.get('layout') if not layout: layout = 'vertical' if request.method == 'POST': form = TestForm(request.POST) form.is_valid() else: form = TestForm() form.fields['title'].widget = BootstrapUneditableInput() return render_to_response('form.html', RequestContext(request, { 'form': form, 'layout': layout, })) def demo_form_inline(request): layout = request.GET.get('layout', '') if layout != 'search': layout = 'inline' form = TestInlineForm() return render_to_response('form_inline.html', RequestContext(request, { 'form': form, 'layout': layout, })) def demo_formset(request): layout = request.GET.get('layout') if not layout: layout = 'inline' DemoFormSet = formset_factory(FormSetInlineForm) if request.method == 'POST': formset = DemoFormSet(request.POST, request.FILES) formset.is_valid() else: formset = DemoFormSet() return render_to_response('formset.html', RequestContext(request, { 'formset': formset, 'layout': layout, })) def demo_tabs(request): layout = request.GET.get('layout') if not layout: layout = 'tabs' tabs = [ { 'link': "#", 'title': 'Tab 1', }, { 'link': "#", 'title': 'Tab 2', } ] return render_to_response('tabs.html', RequestContext(request, { 'tabs': tabs, 'layout': layout, })) def demo_pagination(request): lines = [] for i in range(10000): lines.append(u'Line %s' % (i + 1)) paginator = Paginator(lines, 10) page = request.GET.get('page') try: show_lines = paginator.page(page) except PageNotAnInteger: # If page is not an integer, deliver first page. show_lines = paginator.page(1) except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. show_lines = paginator.page(paginator.num_pages) return render_to_response('pagination.html', RequestContext(request, { 'lines': show_lines, })) def demo_widgets(request): layout = request.GET.get('layout', 'vertical') form = WidgetsForm() return render_to_response('form.html', RequestContext(request, { 'form': form, 'layout': layout, })) 剩下的就是模板目录templates 了,里面的html模板页面较多,我就不一一列出了。不过,现在最兴奋的就是去修改上面的文字,让其看起来更像我们自己的“网站”。 在后面的学习中,我们将以此为基础进行。
Selenium 并不像QTP那样让人一下子就明白是什么?它是编程人员的最爱,但它却对测试新手产生了很大的阻碍。 Selenium 是啥? Selenium RC是啥? Webdriver 又是啥? RC 和 Webdriver 是啥关系? Webdriver 和编程语言啥关系? Selenium 能并行执行脚本嘛? Selenium 能做移动端自动化么? 这里虫师用简单方式,告诉你,他们错综复杂的关系。理顺了它们之间的关系才能真正使用它。 Selenium 是什么? Selenium 是web自动化测试工具集,包括IDE、Grid、RC(selenium 1.0)、WebDriver(selenium 2.0)等。 Selenium IDE 是firefox浏览器的一个插件。提供简单的脚本录制、编辑与回放功能。 Selenium Grid 是用来对测试脚步做分布式处理。现在已经集成到selenium server 中了。 RC和WebDriver 更多应该把它看成一套规范,在这套规范里定义客户端脚步与浏览器交互的协议。以及元素定位与操作的接口。 WebDriver是什么? 对于刚接触selenium自动化测试的同学来说不太容易理解API是什么,它到底和编程语言之是什么关系。 http://www.w3.org/TR/2013/WD-webdriver-20130117/ 当初,在刚学selenium (webdriver)的时候花了一个星期来翻译这个文档,后来也没弄明白,它是啥。其实它就是一层基础的协议规范。 假如说:Webdriver API(接口规范)说,我们要提供一个页面元素id的定位方法。 Ruby的webdriver模块是这么实现的: require "selenium-webdriver" #导入ruby版的selenium(webdriver) find_element(:id, "xx") #id定位方法 C#的webdriver模块是这么实现的: using OpenQA.Selenium; using OpenQA.Selenium.Firefox; //导入C#版的selenium(webdriver) FindElement(By.Id("xx")) //id定位方法 python的webdriver模块是这么实现的: from selenium import webdriver #导入python版的selenium(webdriver) find_element_by_id("xx") #id定位方法 Java的webdriver模块是这么实现的: import org.openqa.selenium.*; import org.openqa.selenium.firefox.FirefoxDriver;//导入java版的selenium(webdriver) findElement(By.id("xx")) //id定位方法 Robot Framework + selenium 因为Robot Framework 对于底层过于封装,所以,我们看不到语言层面的方法定义。所以,Robot Framework 提供给我们的方法如下: 1、导入Robot Framework 版本的selenium(webdriver) 2、使用id方法 Click element Id=xx 需要说明的是 webdriver API 只提供了web页面操作的相关规范,比如元素定位方法,浏览器操作,获取web页元素属性等。 Webdriver 如何组织和执行用例? 对不起,webdriver 不会。 把写好这些操作页面元素的方法(用例)组织起来执行并输入测试结果,是由编程语言的单元测试框架去完成的。如java 的junit和testng单元测试框架,python 的unittest单元测试框架等。 Selenium RC 和WebDriver 什么关系? RC和 WebDriver 类似,都可以看做是一套操作web页面的规范。当然,他们的工作原理不一样。 selenium RC 在浏览器中运行 JavaScript 应用,使用浏览器内置的 JavaScript 翻译器来翻译和执行selenese 命令(selenese 是 selenium 命令集合) 。 WebDriver 通过原生浏览器支持或者浏览器扩展直接控制浏览器。WebDriver 针对各个浏览器而开发,取代了嵌入到被测 Web 应用中的 JavaScript。与浏览器的紧密集成支持创建更高级的测试,避免了JavaScript 安全模型导致的限制。除了来自浏览器厂商的支持,WebDriver 还利用操作系统级的调用模拟用户输入。 看样子webdriver 更牛B一些。为了保持向兼容,所以selenium 2.0中,RC 和webdriver 并存,但说起selenium 2.0 一般指的是webdriver 。 并行与分布式的区别 有同学好奇如何并行的执行测试用例,并行要求“同时”执行多条用例,这个也是由编程语言的多线程技术实现的。 你会问Selenium Grid 不是可以实现分布式执行么? 分布式的概念是写好一条用例可以调用不同的平台执行,如 A电脑上有一个测试用例,可以调用B电脑(linux)的 Firefox浏览器来跑A电脑上的测试用例;也可以调用C电脑(windows)的 Chrome浏览器来跑A电脑上的测试用例。这是分布式的概念。 Selenium如何能做移动端测试么? 这里我们以python 语言为例。 from selenium import webdriver driver= webdriver.Chrome() #获取浏览器驱动。拿到浏览器驱动driver 才能操作浏览器所打找的页面上的元素。 我们把驱动展开是这样的 from selenium import webdriver driver = webdriver.Remote( command_executor='http://127.0.0.1:4444/wd/hub', desired_capabilities={'platform': 'ANY', 'browserName':chrome, 'version': '', 'javascriptEnabled': True }) 驱动里包含了一些参数,代理服务器(URL)平台,浏览器 ,浏览器版本等。 移动端的自动化测试工具Appium 从本质上来讲,appium同样继承了WebDriver API的接口规范。Appium 同样是支持多种编程语言的。这里仍然以python 为例子。 from appium import webdriver #导入python版的 appium(webdriver)模块 #定义驱动的参数 desired_caps = {} desired_caps['platformName'] = 'Android' desired_caps['platformVersion'] = '4.2' desired_caps['deviceName'] = 'Android Emulator' desired_caps['appPackage'] = 'com.android.calculator2' desired_caps['appActivity'] = '.Calculator' driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps) 这一次因为我们操作的是移动端的安卓。所以我们驱动的参数里就要指定平台是'Android' ,版本是4.2 等信息。拿到驱动后,就可以操作安卓上的APP了。
Robot Framework Selenium API 说明: 此文档只是将最常用的UI 操作列出。更多方法请查找selenium 关键字库。 一、浏览器驱动 通过不同的浏览器执行脚本。 Open Browser Htpp://www.xxx.com chrome 浏览器对应的关键字: firefox FireFox ff internetexplorer Internet Explorer ie googlechrome Google Chrome gc chrome opera Opera phantomjs PhantomJS htmlunit HTMLUnit htmlunitwithjs HTMLUnit with Javascipt support android Android iphone Iphone safari Safari 备注: 要想通过不同的浏览打开URL地址,一定要安装浏览器相对应的驱动。如chrome 的驱动: chromedriver.exe 等。 浏览器默认为空时启动FireFox。 二、关闭浏览器 关闭浏览器 Close Browser 关闭当前的浏览器。 关闭所有浏览器 Close All Browsers 关闭所有打开的浏览器和浏览器缓存重置。 三、浏览器最大化 Maximize Browser Window 使当前打开的浏览器全屏。 四、设置浏览器宽、高 Get Window Size 800 600 以像素为单位,第一个参数800表示宽度,第二个参数600表示高度。 五、文本输入 Input Text Xpath=//* [@] 输入信息 Xpath=//* [@] :表示元素定位,定位文本输入框。 六、点击元素 Click Element Xpath=//* [@] Xpath=//* [@] :表示元素定位,定位点击的元素。 七、点击按钮 Click Button Xpath=//* [@] Xpath=//* [@] :表示元素定位,定位点击的按钮。 八、注释 注释1: Comment 注释说明 注释2: # 注释说明 除了使用Comment 关键字进行注释外,Robot framework框架是基于python语言开发的,所以提供了python语言的注释“#”方式。 九、固定时间休眠 Sleep 42 Sleep 1.5 Sleep 2 minutes 10 seconds Sleep表示执行到当前行固定休眠多长时间,以“秒”为单位。 42表示42秒; 1.5 表示1.5秒; 2 minutes 10 seconds 表示2分10秒。 十、等待元素出现在当前页面 Wait Until Page Contains Element Xpath=//* [@] 42 error Xpath=//* [@] :表示元素定位,这里定位出现的元素 42 : 表示最长等待时间。 Error : 表示错误提示,自定义错误提示,如:“元素不能正常显示” 十一、获取title Get Title 获得当前浏览器窗口的title 信息。 这里只获取title 是没有意义的,我们通常会将获取的title 传递给一个变量,然后与预期结果进行比较。从而判断当前脚本执行成功。 十二、获取文本信息 Get Text Xpath=//* [@] Xpath=//* [@] : 定位文本信息的元素。 十三、获取元素属性值 Get Element Attribute id=kw@name id=kw@name : id=kw 表示定位的元素。@nam 获取这个元素的name属性值。 十四、cookie处理 获取cookie get cookies 获得当前浏览器的所有cookie 。 获得cookie值 get cookie value Key_name Key_name : key_name 表示一对cookie中key的name 。 删除cookie delete cookie Key_name 删除key为name 的cookie信息。 删除所有cookies delete all cookies 删除当前浏览器的所有cookie。 添加cookie add cookie Key_name Value_name 添加一对cooke (key:value) 十五、声明变量 ${a} Set Variable hello 定义变量a为hello。 ${a} ${b}= Set Variable hello world 定义变量a为hello ,b为world 。 十六、日志(输出) ${a} Set Variable Hello World log ${a} 在测试报告中输出a变量的值hello word。 十七、获得浏览器窗口宽、高 ${width} ${height} get window size log ${width} log ${height} 获得浏览浏览器窗口宽、高,通过log 将宽高,打印到报告中。 十八、验证 open browser http://www.baidu.com chrome ${title} Get Title should contain ${title} 百度一下,你就知道 Open Browser 通过chrome打开百度首页。 Get Title 获得浏览器窗口的titile ,并赋值给变量${title} Should Contain 比较${title}是否等于“百度一下,你就知道”。 如果item1 不包含 item2 一次或多次,那么失败。 十九、表单嵌套 Select Frame Xpath=//* [@] Unselect Frame Select Frame 进入表单,Xpath=//* [@] 表示定位要进入的表单。 Unselect Frame 退出表单。 二十、下拉框选择 Unselect From List By Value Xpath=//* [@] vlaue Xpath=//* [@] 定位下拉框; Vlaue 选择下拉框里的属性值。 二十一、If分支语句 ${a} Set variable 2 ${b} Set variable 5 run keyword if ${a}>=1 log a大于1 ... ELSE IF ${b}<=5 log b小于等于5 ... ELSE log 上面两个条件都不满足 首先定义两个变量a ,b 分别为 2 和5 。 If 判断 a 大于等于1 ,满足条件log 输出 “a大于1 ”; 不满足上面的条件,接着else if 判断b小于等于5 ,满足条件log 输出 “b小于等于5”; 上面两个条件都不满足,else log输出“上面两个条件都不满足”。 备注:注意sele if 和else前面的三个点点点(...) 二十二、for 循环语句 循环1 :FOR ${i} in range 10 log ${i} 查看结果: 循环变量i 从0 到9 循环10次。 循环2 @{a} create list aaa bbb :FOR ${i} in @{a} log ${i} @{a} 定义为一个字符串列表。 通过in 可遍历非整型(in range) 说明: Log 、if 分支,for 循环并非selenium关键字库的提供的方法,是由BuiltIn包提供。
说明: 不要误认为Robot framework 只是个web UI测试工具,更正确的理解Robot framework是个测试框架,之所以可以拿来做web UI层的自动化是国为我们加入了selenium2的API。比如笔者所处工作中,更多的是拿Robot framework来做数据库的接口测试,当然,需要先将相关的数据库包导入。 那么测试框架的本质是什么?个人觉得有以下几个方面。 1、比较 测试实质就是“比较”,在测试之前需要先写用例,假设经过各种操作之后会得到一个预期的结果,然后,在测试的过程中按照用例的步骤会得到一个实际的结果,拿实际结果与预期结果比较。从而且进一步判断用例的成功与失败。 2、用例的组织 为什么要组职用例,因为用例有很多条,我们或在一个文件中写多条用例,或多个文件中写多条用例,总之要很好的把这些用例组织起,自动化用例是给程序去跑的,所以,更应该规范的组织起来。 3、执行结果展示 用例跑完了,成功了,失败了?用例执行到哪一步失败了?总要把这些信息展示给用户吧。 ============= 回到主题,在Robot framework中元素的定位。 因为Robot framework 引入的selenium2 包,所以,假如我们学过selenium 的话,定位是一样的。因为没找到相关资料,所以,经过验证id 、name ,xpath 、css 四种定位方式是可以的,尤其后两种是“万能的”,所以可以解决99%的定位问题。 id 和name 定位 假如把一个元素看作一个人的话,id 和name可以看作一个人的身份证号和姓名。当然,这些属性值是否唯一要看前端工程师如何设计了。 百度搜索框和搜索按钮 …… <input id="kw1" class="s_ipt" type="text" maxlength="100" name="wd" autocomplete="off"> …… <input id="su1" class="bg s_btn" type="submit" onmouseout="this.className='bg s_btn'" onmousedown="this.className='bg s_btn s_btn_h'" value="百度一下"> …… 根据上面的例子,百度输入框可以取id 或 name进行定位。(前提是id和name的值在当页面上唯一) id = kw1 name = wd 在Robot framework 中就是这样写的: Input Text id=kw1 robot framework学习 input text name=wd robot framework学习 Input text 用于输入框的关键字,“robot framework学习”是要给输入框输入的内容。 百度按钮只id数据可以利用: Id=su1 Click Button id=su1 Click Button是按钮点击的关键字。 xpath定位 假如,一个人没身份证号没名字怎么找呢?想想你是怎么找朋友吃饭的,他手机不通,电话不回呢?直接上他家去呗,那你一定有他家住址,xx市xx区xx路xx号。Xpath 就可以通过这种层级关系找到元素。 来看看百度输入框在整个页面上的位置吧: <html> <head> <body link="#0000cc"> <div id="wrapper" style="display: block;"> <div id="debug" style="display:block;position:absolute;top:30px;right:30px;border:1px solid;padding:5px 10px;"></div> <div id="u"> <div id="head"> <div id="content" style="display: block;"> <div id="u1" style="display: block;"> <div id="m"> <p id="lg"> <p id="nv"> <div id="fm"> <form id="form1" class="fm" action="/s" name="f1"> <span class="bg s_ipt_wr"> <input id="kw1" class="s_ipt" type="text" maxlength="100" name="wd" autocomplete="off"> 1、Xpath的绝对路径: Xpath = /html/body/div[1]/div[4]/div[2]/div/form/span[1]/input 我们可以从最外层开始找,html下面的body下面的div下面的第4个div下面的....input标签。通过一级一级的锁定就找到了想要的元素。 2、Xpath的相对路径: 绝对路径的用法往往是在我们迫不得已的时候才用的。大多时候用相对路径更简便。 2.1、元素本身: Xpath同样可以利用元素自身的属性: Xpath = //*[@id=’kw1’] //表示某个层级下,*表示某个标签名。@id=kw1 表示这个元素有个id等于kw1 。 当然,一般也可以制定标签名: Xpath = //input[@id=’kw1’] 元素本身,可以利用的属性就不只局限为于id和name ,如: Xpath = //input[@type=’text’] Xpath = //input[@autocomplete=’off’] 但要保证这些元素可以唯一的识别一个元素。 2.2、找上级: 当我们要找的一个人是个刚出生的婴儿,还没起名子也没有入户口(身份证号),但是你会永远跟在你父亲的身边,你的父亲是有唯一的名字和身份证号的,这样我们可以先找到你父亲,自然就找到你的。 元素的上级属性为: <form id="form1" class="fm" action="/s" name="f1"> <span class="bg s_ipt_wr"> <input id="kw1" class="s_ipt" type="text" maxlength="100" name="wd" autocomplete="off"> 找爸爸: xpath = //span[@class=’bg s_ipt_w’]/input 如果爸爸没有唯一的属性,可以找爷爷: xpath = //form[@id=’form1’]/span/input 这样一级一级找上去,直到html ,那么就是一个绝对路径了。 2.3、布尔值写法: 如果一个人的姓名不是唯一的,身份证号也不是唯一的,但是同时叫张三 并且 身份证号为123 的人却可以唯一的确定一个人。那么可以这样写: Xpath = //input[@id=’kw1’ and @name=’wd’] 可以and ,当然也可以or : Xpath = //input[@id=’kw1’ or @name=’wd’] 但or的实际意义不太。我们一般不需要说,找的人名字或者叫张三,或者身份证号是123 也可以。 Robot framework 中的写法: Input Text xpath = //*[@id=’kw1’] robot framework学习 input text xpath = //span[@class=’bg s_ipt_w’]/input robot framework学习 input text xpath = //input[@id=’kw1’ and @name=’wd’] robot framework学习 CSS定位 Css的定位更灵活,因为他它用到的更多的匹配符和规格。 http://www.w3school.com.cn/cssref/css_selectors.asp 选择器 例子 例子描述 .class .intro 选择 class="intro" 的所有元素。 #id #firstname 选择 id="firstname" 的所有元素。 * * 选择所有元素。 element p 选择所有 <p> 元素。 element,element div,p 选择所有 <div> 元素和所有 <p> 元素。 element element div p 选择 <div> 元素内部的所有 <p> 元素。 element>element div>p 选择父元素为 <div> 元素的所有 <p> 元素。 element+element div+p 选择紧接在 <div> 元素之后的所有 <p> 元素。 [attribute] [target] 选择带有 target 属性所有元素。 [attribute=value] [target=_blank] 选择 target="_blank" 的所有元素。 [attribute~=value] [title~=flower] 选择 title 属性包含单词 "flower" 的所有元素。 [attribute|=value] [lang|=en] 选择 lang 属性值以 "en" 开头的所有元素。 同样以百度输入框的代码,我们来看看CSS如何定位。 <form id="form1" class="fm" action="/s" name="f1"> <span class="bg s_ipt_wr"> <input id="kw1" class="s_ipt" type="text" maxlength="100" name="wd" autocomplete="off"> id定位: css=#kw1 class定位: css=.s_ipt 其它属性: css=[name=wd] css=[type=text] css=[autocomplete=off] 父子定位: css=span > input css=form > span > input 根据标签名定位: css=input Robot framework 中的写法: Input Text css=#kw1 robot framework学习 input text css=.s_ipt robot framework学习 input text css=[name=wd] robot framework学习 同样一个元素,根基CSS的不同规则,可能有几十上百种写法。CSS更灵活强大,但是相比xpath 的学习成本为更高。但是css和xpath 两种定位方式是一定要学会一种,不然你的自动化工作更无法开展。
最近工具中用Robot Framework框架来做自动化,所以,花时间学习了一下。 =======所需环境=================== Python: https://www.python.org/ RF框架是基于python 的,所以一定要有python环境。 Robot framework : https://pypi.python.org/pypi/robotframework/2.8.5 这个不是解释了,RF框架。虽然在做基于UI的自动化时,它展现出来的很像QTP,我之前也以为它和QTP差不多,仔细了解你会发展它能做的事情还是很多的。就像初学selenium 者,会误以为selenium 就是selenium IDE。 wxPython : http://www.wxpython.org/download.php Wxpython 是python 非常有名的一个GUI库,因为RIDE 是基于这个库开发的,所以这个必须安装。 Robot framework-ride https://pypi.python.org/pypi/robotframework-ride RIDE就是一个图形界面的用于创建、组织、运行测试的软件。 Robot framework-selenium2library: https://pypi.python.org/pypi/robotframework-selenium2library/1.5.0 RF-seleniumlibrary 可以看做RF版的selenium 库,selenium (webdriver)可以认为是一套基于web的规范(API),所以,RF 、appium 等测试工具都可以基于这套API进行页面的定位与操作。 ---------------------- 可以通过python 的pip工具包进行安装: >pip install robotframework-selenium2library 如果初次接触上面的东西的话,觉得装的东西有点多。 如果之前有了解过python 或selenium的话就不会有这样的感觉。 ================================================ 在你安装好RF-ride之后,桌面就会生成一个RIDE图标。双击启动,界面如下: 下面我们就一步一步的创建第一条用例,至于细节不多解释,只是对RF框架写用例有个感性的认识。 创建测试项目 选择菜单栏file----->new Project Name 输入项目名称。 Type 选择Directory。 创建测试套件 右键点击“测试项目”选择new Suite 选项 Name 输入项目名称。 Type 选择File。 创建测试用例 右键点击“测试项目”选择new Test Case 用例只需要输入用例name ,点击OK即可。 导入selenium2library库 因为RF框架编写基于web 的测试用例,所以,我们需要selenium 的库支持。所以,我们在使用的过程中需要加载selenium2library库。 在“测试套件”的Edit标签页,点击“Library”按钮,弹出输入框,Name输入:Selenium2Library,点击OK 完。 如果导入的库显示为红色,表示导入的库不存在。如果是黑色则表示导入成功。 编写用例 下面就可以开始写我们的用例了,可是怎么写呢?我们可以通过按F5 快捷键来查询脚本的关键字。如果你接触过QTP 或 selenium IDE 等自动化工具的话,应该会有一些思路。 如上图,自动化脚本从打开浏览器开发,如上图,我想打开一个浏览器,想的是“open”为关键字进行搜索,结果找到了一个“Open Browser”的关键字,点击这个关键字,想显示它的用法和说明。 根据说明,我们来尝试创建这个打开浏览器的操作吧: “Open Browser”变蓝了,说明它是一个合法的关键字,后面有一个方框是红色的,表示这个参数不能缺省的。通过说明信息中,我发现它需要一个url 地址是必填的,当然还需要指定browser (默认不填为 friefox) 更多关键的使用,请参考相关API 文档。这里不过多介绍。按照上面的方法。创建百度搜索用例如下: 运行测试用例 勾选当前需要运行的测试用例,点击工具栏运行按钮,如果只运行单个用例的话,也可以切换到用例的Run标签页,点击“start”按钮。 运行信息: 运行信息显示会生成三个文件:Output.xml、Log.html、Report.html 我们重点查看Log.html和Report.html ,Log.html更关注脚本的执行过程的记录,Report.html更关注脚本的执行结果的展示。 赶快打开你的测试报告看看效果吧! ================================================================================ 错误: command: pybot.bat --argumentfile c:\users\keikei\appdata\local\temp\RIDEama2ym.d\argfile.txt --listener D:\Python27\lib\site-packages\robotide\contrib\testrunner\TestRunnerAgent.py:52418 E:robot\测试项目 解决: 将“C:\Python27\Scripts ”添加到PATH环境变量中。命令提示符号查看,RF版本。提示pybot 不是内部命令,说明环境变量设置有问题。
说明: 从这一篇开始就不再完整的介绍django项目的创建过程了,因为前面几篇博客中都详细的介绍了这个创建过程,套路都是一样的,熟悉了这个套路,后面要做的是一些细节技术点的学习和练习。 上一节讲到了django中如何使用cookie来记录用户登录信息,这一节来了解session是如何来记录用户登录信息的。 创建项目,创建应用,设置settings.py的过程不再介绍。 项目目录: 设置URL 设置urls.py 文件如下: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'csvt11.views.home', name='home'), # url(r'^blog/', include('blog.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^login/$', 'online.views.login'), url(r'^index/$', 'online.views.index'), url(r'^logout/$', 'online.views.logout'), ) 进行数据库的同步 因为本例中,我们不需要创建数据库表(当然你可以参考前面几章的例子创建用户登录的数据库表),所以,这里直接执行数据库的同步。 自动创建了一个叫django_session的表,这个表里就是用于存放我们的session信息的。 创建视图 views.py #coding=utf-8 from django.shortcuts import render from django.shortcuts import render,render_to_response from django.http import HttpResponse,HttpResponseRedirect from django import forms class UserForm(forms.Form): username = forms.CharField() #用户登录 def login(req): if req.method == "POST": uf = UserForm(req.POST) if uf.is_valid(): username = uf.cleaned_data['username'] #把获取表单的用户名传递给session对象 req.session['username'] = username return HttpResponseRedirect('/index/') else: uf = UserForm() return render_to_response('login.html',{'uf':uf}) #登录之后跳转页 def index(req): username = req.session.get('username','anybody') return render_to_response('index.html',{'username':username}) #注销动作 def logout(req): del req.session['username'] #删除session return HttpResponse('logout ok!') 这里用到的就是session创建和删除,代码中有注释。视图是动能实现的核心逻辑,这里调用到了session的相关方法,非常简单,需要说明的是session 是字典的形式存在的,比如一个sessionid 对应一个信息(比如,用户名,密码,添加到购物车的商品等。) 创建模板 login.html <form method = 'post'> {{uf.as_p}} <input type="submit" value = "ok"/> </form> index.html <div> <h1>welcome {{username}}</h1> <a href="/logout">logout</a> </div> 访问登录 http://127.0.0.1:8000/login/ 这里没有判断用户密码是否正常的逻辑,所以,输入任意信息都可登录。 查看浏览器session id 查看数据库 看查,session 是用户登录的用户名保存服务器端的数据库中,而客户端(浏览器)产生的只是一个session id ,程序通过读取客户端的session id 来查找对应的用户名,并返回给客户端,从而在客户端的信息。在数据库中并没有看到刚才登录的用户名(Tom),标红色下划线的就是,只是对其进行了加密,所以会看到一串很长的大小写字符串。 登录成功: 点击logout退出: 再次访问index页 在index 页面,点击“logout” 退出后,就删除了客户端的session id 信息,所以,再访问index 页面,就是会看到“weclome anybody”的提示。 问题: 按照正常的逻辑,用户登录是不准访问登录成功的页面(index),这一块就涉及到django 的“访问限制”的相关方法。
在开始讲述这一年多的经历的过程之间,我又回顾了之前的经历,以便把比较好的把故事的衔接,需要说明的是,我并没什么高大上的经历来吹牛皮,只是做为一个普普通通的软件测试员,来记录自己的经历而已。 关于学历 应该是在入职新公司前报考的自考,学历一直是我的硬伤,所以,就想通过自考的方式来弥补,对于搞技术的来说,尤其已经在这个行业混了几年的人来说,学历真有还很重要么?这得看公司。有些公司不在意学历,有些公司没有就是不行。至少在我面试的不少公司来看,有时候确实挺重要的,有些给钱多的,比如金融证券类的公司,学历必须的;有些人才济济,做技术的都想削尖脑袋往里进的,比如,华为、腾讯。 因为自考是个挺花时间的事情,几乎每个星期天都要上课,所以,几乎是没有休息时间的,要么上班要么上课。当然也有空闲,自考完了会有两周不用上课,有了一份稳定的工作之后,渐渐我的就开始怀疑,自考就真有用么,占具了我大量的时间,就为了一张纸,也许把这些时间用来学技术更有用。 直到我再次找工作的时候,我不再怀疑了,没这张纸,到嘴肉又飞了。证还没下来,为啥又急着跳,这次不是我主动的,情非得已,最后再说。 关于学历就说这么多,这只是我的个人经历的感受。 学习python 回顾了上一篇的经历,说要认真的学一门语言,我基本做到了。在这一年多的时间里,我没再关心眼花缭乱的各种测试技术。把除了大多时间与精力花在了学习python 上。说有多精通谈不上,但写写自动化脚本,实现个小功能问题不大。 从2013年4月份入职新公司说起,入职什么的一两个月里没有特别紧迫的事情,处于半打酱油状态,后来调到web社区组后,了解项目是基于python实现的,于是,开始学python,简单易学,相关资料文档也丰富,所以学起来并不困难,但坚持很重要。 其实,我已经不止一次的传达我的看法,以及我验证的结果:测试人员面对的技术太多,我们真正用到的技术又很少,所以,我们就很容易三心二意,今天听别人讨论这个技术流B ,就学这个;明天又听说那个技术流B又去学那个。混了几年发现仍然缺乏核心竞争力。会写文档不是核心竞争力,会写用例不是核心竞争力,会用某种测试工具也不是核心竞争力。 听我的,抛开那些所谓高大上的测试技术吧。专心学一门语言,一年后,你一定会来感谢我的。测试人员如何学语言? 关于自动化 你一定在抱怨,买了本编程书,上面的代码也都看明白了,也都敲了几遍。但还是不能像开发一样写程序,而且更重要的是学了又不上,过段时间又忘记。 好,我告诉我是怎么做的。 我大概花了一个月找来python 的一本基础教程。学完大概就是上面所说的状态。然后,我发现我们web项目挺适合做自动化的,selenium webdriver 本身是支持python来做自动化的,但关于webdriver + python 来做自动化的中文资料并不多,学习起来颇为痛苦,webdriver API上的方法,没有python写法的实例了。所以,花了不少时间来学习API 。 熟悉页面上各种元素的操作,问题又回到python上,以至于当初的去纠结如何用python来循环读取一个文件里的数据,还好这样例子很容易找到。突然有一天在一次挣扎过后,我醍醐灌顶的明白了用程序来解决问题。 我在学习的过程中更多的是以需求为驱动去解决实际问题。过程很痛苦,结果很爽快,在反复的痛苦-爽快的过程中,你就具备了编程能力。 因为坚持专一,这也就是为什么我可以在半年后开始向别人讲 selenium+python如何实现自动化的课程。 仍然不断的有人问,为啥不学QTP ,简单易学,功能强大。因为我不仅仅是为了做自动化而在学自动化。 编程如写文章,识字的人都能看懂文章,我们在不断写作的过程中,模仿的过程中学会了写出优秀的作品;懂编程语法的人都能把程序看个大概,只有在不断的练习、不断的模仿中才能写出了健壮高效的软件。 关于文档 《selenium 2 python 自动化测试实战》应该可以体现我这一年多来的技术积累。因为最初博客写了十几篇 selenium webdriver python版的webdriver 如何操作页面各种元素。为了方便别人阅读,所以,整理了。 selenium webdriver (python) 第一版PDF 后面,又学到了一些东西,加到了里面,于是又有了: selenium webdriver (python) 第二版 再后面,又了加一些技术,于是又有了: selenium webdriver (python) 第三版 在这个学习的过程中,兔子给我不少帮助,并且向我介绍了他们的测试框架之后,我非常激动,觉得这技术非常有用。前三版的文档也得到了他的鼓励。这次我准备玩个大的(原本是想投稿出版社的),并没有急于第四版、第五版这样更新下去。花了相当的时间和精力攻破一个个技术点。不单单是webdriver ,添加selenium IDE 和selenium grid 的使用。形成了一套比较完整的知识结构,于是,有了: 《selenium2 python 自动化测试实战》 --new 名字发生的变化,好多人认为他们不是一个文档。好吧!他们是继承关系,“实战”继承了前面几个文档的所有东西。最新版已经扩充到360页。 关于分享 Selenium + Python 的自动化测试分享也是2013年底开始搞的,2013年年初的时候,我想总结自己的测试经验录制一套《web测试指南》的视频,很紧张,效果就非常差,录制了5节后就停掉了;讲课能力一直是我的一个短板,写文章的能力相比要好很多,这也主要是这几年不间断写的积累。所以,视频别人听了之后,惊呼文不对声。 后来,乙醇要做Selenium + Python 自动化测试的分享,因为之前我向他请教过这方面的问题。所以,他希望我来讲,我很高兴的接受了这个任务。第一期的效果每是很差的,到目前的第三期已经有了很多的进步。可以讲的内容也在不断扩展。 关于离开 我没用“离职”而是“离开”,这个原因比较纠结。这个公司是我目前为止待着最舒服的一个公司。公司福利,公司文化,工作强度,同事关系都很好。所以,我才有精力做这么多事儿,向团队分享自动化,对项目进行自动化。 年初的时候有过跳槽的躁动,调薪之后,新项目规划出来之后,就决心继续干下去。可惜意外的情况打破了这些。项目没了,新的项目胎死腹中。在压抑的环境待一个月多,无所事事。然后,我就离开了。 生活继续,工作继续,学习继续。我的经历继续。 ==============================================
笔试遇到的三道测试开发题,虽然都不难,但关键还是思路吧!我想在开发东西的时候应该具备的就是思路,有了思路尝试去写,或查相关文档或代码,在此基础上需要不断调整最终达到需求。思路又是在不断练习中获得的。 在整个面试过程中,笔试往往不是重点,但从一笔试可以看出一个人平时对基础知识的积累。 再说明的一点是,对于下面的问题,有的要求用php,有的要求java,但我用python实现的。很多时候公司并不是在意你必须用哪种语言去实现,语言只是工具,用来解决问题了,关键是否有思路。 验证邮箱格式 验证邮箱的格式,不同语言的实现大同小异,通过正则表达式是最快捷的匹配方式,但对于不熟悉正则的同学看着一长串匹配符还是比较头痛的,其实也没那么恐怖。 熟悉python 中正则表达式的常用个匹配符 先看一下邮箱的一般格式: x@x.x x 表示一个或多个字符或数字。 1)第一个x可以字母数字 2)第二个x可以字母数字 3)第二个x可以字母,如.com,.cn,.net...等结尾 “@”和“.” 把内x拆成三部份。 整个邮箱长度最少等于5个字符。 代码如下: #coding=utf-8 import re ''' [a-zA-Z0-9] 匹配大小写字母与数字 [a-zA-Z] 匹配大小写字母 \@ a\@b a@b (字符转义) \. a\.b a.b (字符转义) ''' def emails(e): if len(e)>= 5: if re.match("[a-zA-Z0-9]+\@+[a-zA-Z0-9]+\.+[a-zA-Z]",e) !=None: return '邮箱格式正确!' return '邮箱格式有误' e = raw_input("请输入email:") print e a = emails(e) print a 运行结果: >>> ================================ RESTART ================================ >>> 请输入email:12@22.22 12@22.22 邮箱格式有误 >>> ================================ RESTART ================================ >>> 请输入email:xx@xx.com abc@126.com 邮箱格式正确! >>> ================================ RESTART ================================ >>> 请输入email:123@126.com 123@126.com 邮箱格式正确! ....... 获得一个URL地址的扩展名 如: http://www.cnblogs.com/fnng/archive/2013/05/20/3089816.html 的扩展名为html 对于这个问题同样使用正则式来解决 import re def strings(url): listt = ['.php','.html','.asp','.jsp'] for lis in listt: suffix = re.findall(lis,url) if len(suffix)>0: return lis url = 'http://www.cnblogs.com/fnng/archive/2013/05/20/3089816.html' a = strings(url) print a 运行结果: .html 获得当前时间的前一天(或前一秒) 如果当前时间为:2014-6-11 17:12:45 前一天为:2014-6-10 17:12:45 前一秒为:2014-6-11 17:12:44 #coding=utf-8 import time import datetime #打印当前时间 print time.ctime() #当前时间 now_time = datetime.datetime.now() print now_time #昨天的现在 yesterday = now_time + datetime.timedelta(days = -1) print yesterday #现在的前一秒 now_old = now_time + datetime.timedelta(seconds = -1) print now_old 运行结果: Wed Jun 11 17:21:07 2014 2014-06-11 17:21:07.750000 2014-06-10 17:21:07.750000 2014-06-11 17:21:06.750000 ====== 这个是在笔试过程中比较有印象的几道题,当时也写了个大概,没经过调试应该有问题,或不太符合需求,这里标记一下!想了想还是做为一篇博客发表一下。后续有时间会详细讲述跳槽经历以及最近一年多的感悟。
经过前面几节的练习,我们已经熟悉了django 的套路,这里来实现一个比较完整的登陆系统,其中包括注册、登陆、以及cookie的使用。 本操作的环境: =================== deepin linux 2013(基于ubuntu) python 2.7 Django 1.6.2 =================== 创建项目与应用 #创建项目 fnngj@fnngj-H24X:~/djpy$ django-admin.py startproject mysite5 fnngj@fnngj-H24X:~/djpy$ cd mysite5 #在项目下创建一个online应用 fnngj@fnngj-H24X:~/djpy/mysite5$ python manage.py startapp online 目录结构如下: 打开mysite5/mysite5/settings.py文件,将应用添加进去: # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'online', ) 设计数据库 打开mysite5/online/models.py文件,添加如下内容: from django.db import models # Create your models here. class User(models.Model): username = models.CharField(max_length=50) password = models.CharField(max_length=50) def __unicode__(self): return self.username 创建数据库,创建User表,用户名和密码两个字段。 下面进行数据库的同步: fnngj@fnngj-H24X:~/djpy/mysite5$ python manage.py syncdb Creating tables ... Creating table django_admin_log Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_groups Creating table auth_user_user_permissions Creating table auth_user Creating table django_content_type Creating table django_session Creating table online_user You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes 输入yes/no Username (leave blank to use 'fnngj'): 用户名(默认当前系统用户名) Email address: fnngj@126.com 邮箱地址 Password: 密码 Password (again): 确认密码 Superuser created successfully. Installing custom SQL ... Installing indexes ... Installed 0 object(s) from 0 fixture(s) 最后生成的 online_user 表就是我们models.py 中所创建的User类。 配置URL 打开mysite5/mysite5/urls.py: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite5.views.home', name='home'), url(r'^admin/', include(admin.site.urls)), url(r'^online/', include('online.urls')), ) 在mysite5/online/目录下创建urls.py文件: from django.conf.urls import patterns, url from online import views urlpatterns = patterns('', url(r'^$', views.login, name='login'), url(r'^login/$',views.login,name = 'login'), url(r'^regist/$',views.regist,name = 'regist'), url(r'^index/$',views.index,name = 'index'), url(r'^logout/$',views.logout,name = 'logout'), ) http://127.0.0.1:8000/online/ 登陆页 http://127.0.0.1:8000/online/login/ 登陆页 http://127.0.0.1:8000/online/regist/ 注册页 http://127.0.0.1:8000/online/index/ 登陆成功页 http://127.0.0.1:8000/online/logout/ 注销 创建视图 打开mysite5/online/views.py 文件: #coding=utf-8 from django.shortcuts import render,render_to_response from django.http import HttpResponse,HttpResponseRedirect from django.template import RequestContext from django import forms from models import User #表单 class UserForm(forms.Form): username = forms.CharField(label='用户名',max_length=100) password = forms.CharField(label='密码',widget=forms.PasswordInput()) #注册 def regist(req): if req.method == 'POST': uf = UserForm(req.POST) if uf.is_valid(): #获得表单数据 username = uf.cleaned_data['username'] password = uf.cleaned_data['password'] #添加到数据库 User.objects.create(username= username,password=password) return HttpResponse('regist success!!') else: uf = UserForm() return render_to_response('regist.html',{'uf':uf}, context_instance=RequestContext(req)) #登陆 def login(req): if req.method == 'POST': uf = UserForm(req.POST) if uf.is_valid(): #获取表单用户密码 username = uf.cleaned_data['username'] password = uf.cleaned_data['password'] #获取的表单数据与数据库进行比较 user = User.objects.filter(username__exact = username,password__exact = password) if user: #比较成功,跳转index response = HttpResponseRedirect('/online/index/') #将username写入浏览器cookie,失效时间为3600 response.set_cookie('username',username,3600) return response else: #比较失败,还在login return HttpResponseRedirect('/online/login/') else: uf = UserForm() return render_to_response('login.html',{'uf':uf},context_instance=RequestContext(req)) #登陆成功 def index(req): username = req.COOKIES.get('username','') return render_to_response('index.html' ,{'username':username}) #退出 def logout(req): response = HttpResponse('logout !!') #清理cookie里保存username response.delete_cookie('username') return response 这里实现了所有注册,登陆逻辑,中间用到cookie创建,读取,删除操作等。 创建模板 先在mysite5/online/目录下创建templates目录,接着在mysite5/online/templates/目录下创建regist.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>注册</title> </head> <body> <h1>注册页面:</h1> <form method = 'post' enctype="multipart/form-data"> {% csrf_token %} {{uf.as_p}} <input type="submit" value = "ok" /> </form> <br> <a href="http://127.0.0.1:8000/online/login/">登陆</a> </body> </html> mysite5/online/templates/目录下创建login.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>登陆</title> </head> <body> <h1>登陆页面:</h1> <form method = 'post' enctype="multipart/form-data"> {% csrf_token %} {{uf.as_p}} <input type="submit" value = "ok" /> </form> <br> <a href="http://127.0.0.1:8000/online/regist/">注册</a> </body> </html> mysite5/online/templates/目录下创建index.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title></title> </head> <body> <h1>welcome {{username}} !</h1> <br> <a href="http://127.0.0.1:8000/online/logout/">退出</a> </body> </html> 设置模板路径 打开mysite5/mysite5/settings.py文件,在底部添加: #template TEMPLATE_DIRS=( '/home/fnngj/djpy/mysite5/online/templates' ) 使用功能 注册 先注册用户: 注册成功,提示“regist success!!” 登陆 执行登陆操作,通过读取浏览器cookie 来获取用户名 查看cookie 登陆成功 通过点击“退出”链接退出,再次访问http://127.0.0.1:8000/online/index/ 将不会显示用户名信息。
前言 对于web开来说,用户登陆、注册、文件上传等是最基础的功能,针对不同的web框架,相关的文章非常多,但搜索之后发现大多都不具有完整性,对于想学习web开发的新手来说不具有很强的操作性;对于web应用来说,包括数据库的创建,前端页面的开发,以及中间逻辑层的处理三部分。 本系列以可操作性为主,介绍如何通过django web框架来实现一些简单的功能。每一章都具有完整性和独立性。希望新手在动手做的过程中体会web开发的过程,过程中细节请参考相关文档。 本操作的环境: =================== deepin linux 2013(基于ubuntu) python 2.7 Django 1.6.2 =================== 在上一节中介绍了django 如何实现用户注册,用户注册好了账号,就可以拿着注册的账号去登录了。这一节介绍如何实现用户登录。 创建项目与应用 #创建项目 fnngj@fnngj-H24X:~/djpy$ django-admin.py startproject mysite4 fnngj@fnngj-H24X:~/djpy$ cd mysite4 #在项目下创建一个login应用 fnngj@fnngj-H24X:~/djpy/mysite4$ python manage.py startapp login 项目目录结构如下: 打开mysite4/mysite4/settings.py文件,将应用添加进去: # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'login', ) …… #顺便注释csrf MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', #'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) 设计model(数据库) 打开mysite4/login/models.py文件,添加如下内容 from django.db import models from django.contrib import admin # Create your models here. class User(models.Model): username = models.CharField(max_length=50) password = models.CharField(max_length=50) admin.site.register(User) 创建一个User表,有两个字段username、password 然后,进行数据库的同步: fnngj@fnngj-H24X:~/djpy/mysite4$ python manage.py syncdb Creating tables ... Creating table django_admin_log Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_groups Creating table auth_user_user_permissions Creating table auth_user Creating table django_content_type Creating table django_session Creating table login_user You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes 输入yes/no Username (leave blank to use 'fnngj'): 用户名(默认当前系统用户名) Email address: fnngj@126.com 邮箱地址 Password: 密码 Password (again): 确认密码 Superuser created successfully. Installing custom SQL ... Installing indexes ... Installed 0 object(s) from 0 fixture(s) 配置URL 打开mysite4/mysite4/urls.py: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite5.views.home', name='home'), # url(r'^blog/', include('blog.urls')), url(r'^admin/', include(admin.site.urls)), ) 启动服务 fnngj@fnngj-H24X:~/djpy/mysite4$ python manage.py runserver Validating models... 0 errors found May 21, 2014 - 14:31:32 Django version 1.6.2, using settings 'mysite4.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. 访问admin http://127.0.0.1:8000/admin/ 登录用户名和密码为我们进行数据库同步时所设置的信息。 登录之后,选择add 添加用户。 创建用户,点击save 再次打开mysite4/login/models.py文件,添加如下内容 from django.db import models from django.contrib import admin # Create your models here. class User(models.Model): username = models.CharField(max_length=50) password = models.CharField(max_length=50) class UserAdmin(admin.ModelAdmin): list_display = ('username','password') admin.site.register(User,UserAdmin) 再次刷新,admin后台,如下显示: 创建视图 现在我们已经生成了一个用户信息表,下面要做的就是设计用户登录功能了。 打开mysite4/login/views.py 文件 #coding=utf-8 from django.shortcuts import render,render_to_response from django.http import HttpResponseRedirect from login.models import User from django import forms #定义表单模型 class UserForm(forms.Form): username = forms.CharField(label='用户名:',max_length=100) password = forms.CharField(label='密码:',widget=forms.PasswordInput()) #登录 def login(request): if request.method == 'POST': uf = UserForm(request.POST) if uf.is_valid(): #获取表单用户密码 username = uf.cleaned_data['username'] password = uf.cleaned_data['password'] #获取的表单数据与数据库进行比较 user = User.objects.filter(username__exact = username,password__exact = password) if user: return render_to_response('success.html',{'username':username}) else: return HttpResponseRedirect('/login/') else: uf = UserForm() return render_to_response('login.html',{'uf':uf}) 上面登录的核心是比较,拿到用户填写的表单数据(用户名、密码)与数据库User表中的字段进行比较,根据比较结果,如果成功跳转到success.html页面,如果失败还留在原来页面login.html 。 创建模板 根据视图层的要求,我们需要创建两个页面,success.html 和login.html 先在mysite4/login/目录下创建templates目录,接着在mysite4/login/templates/目录下创建login.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>登录</title> </head> <style type="text/css"> body{color:#efd;background:#453;padding:0 5em;margin:0} h1{padding:2em 1em;background:#675} h2{color:#bf8;border-top:1px dotted #fff;margin-top:2em} p{margin:1em 0} </style> <body> <h1>登录页面:</h1> <form method = 'post' enctype="multipart/form-data"> {{uf.as_p}} <input type="submit" value = "ok" /> </form> </body> </html> 在mysite4/login/templates/目录下创建success.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title></title> </head> <body> <h1>恭喜{{username}},登录成功!</h1> </form> </body> </html> 设置模板路径 打开mysite4/mysite4/settings.py文件,在底部添加: #template TEMPLATE_DIRS=( '/home/fnngj/djpy/mysite4/login/templates') 配置URL 打开mysite4/mysite4/urls.py: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite5.views.home', name='home'), url(r'^login/', include('login.urls')), url(r'^admin/', include(admin.site.urls)), ) 创建文件mysite4/login/urls.py from django.conf.urls import patterns, url from login import views urlpatterns = patterns('', url(r'^$', views.login, name='login'), ) 启动服务(python manage.py runserver), 访问登录页面(http://127.0.0.1:8000/login/) 用户名、密码正确,点击OK ,跳转到登录成功页面:
前言 对于web开来说,用户登陆、注册、文件上传等是最基础的功能,针对不同的web框架,相关的文章非常多,但搜索之后发现大多都不具有完整性,对于想学习web开发的新手来说不具有很强的操作性;对于web应用来说,包括数据库的创建,前端页面的开发,以及中间逻辑层的处理三部分。 本系列以可操作性为主,介绍如何通过django web框架来实现一些简单的功能。每一章都具有完整性和独立性。希望新手在动手做的过程中体会web开发的过程,过程中细节请参考相关文档。 本操作的环境: =================== deepin linux 2013(基于ubuntu) python 2.7 Django 1.6.2 =================== 在上一节中介绍了django 如何实现用户注册,用户注册好了账号,就可以拿着注册的账号去登录了。这一节介绍如何实现用户登录。 创建项目与应用 #创建项目 fnngj@fnngj-H24X:~/djpy$ django-admin.py startproject mysite4 fnngj@fnngj-H24X:~/djpy$ cd mysite4 #在项目下创建一个login应用 fnngj@fnngj-H24X:~/djpy/mysite4$ python manage.py startapp login 项目目录结构如下: 打开mysite4/mysite4/settings.py文件,将应用添加进去: # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'login', ) …… #顺便注释csrf MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', #'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) 设计model(数据库) 打开mysite4/login/models.py文件,添加如下内容 from django.db import models from django.contrib import admin # Create your models here. class User(models.Model): username = models.CharField(max_length=50) password = models.CharField(max_length=50) admin.site.register(User) 创建一个User表,有两个字段username、password 然后,进行数据库的同步: fnngj@fnngj-H24X:~/djpy/mysite4$ python manage.py syncdb Creating tables ... Creating table django_admin_log Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_groups Creating table auth_user_user_permissions Creating table auth_user Creating table django_content_type Creating table django_session Creating table login_user You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes 输入yes/no Username (leave blank to use 'fnngj'): 用户名(默认当前系统用户名) Email address: fnngj@126.com 邮箱地址 Password: 密码 Password (again): 确认密码 Superuser created successfully. Installing custom SQL ... Installing indexes ... Installed 0 object(s) from 0 fixture(s) 配置URL 打开mysite4/mysite4/urls.py: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite5.views.home', name='home'), # url(r'^blog/', include('blog.urls')), url(r'^admin/', include(admin.site.urls)), ) 启动服务 fnngj@fnngj-H24X:~/djpy/mysite4$ python manage.py runserver Validating models... 0 errors found May 21, 2014 - 14:31:32 Django version 1.6.2, using settings 'mysite4.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. 访问admin http://127.0.0.1:8000/admin/ 登录用户名和密码为我们进行数据库同步时所设置的信息。 登录之后,选择add 添加用户。 创建用户,点击save 再次打开mysite4/login/models.py文件,添加如下内容 from django.db import models from django.contrib import admin # Create your models here. class User(models.Model): username = models.CharField(max_length=50) password = models.CharField(max_length=50) class UserAdmin(admin.ModelAdmin): list_display = ('username','password') admin.site.register(User,UserAdmin) 再次刷新,admin后台,如下显示: 创建视图 现在我们已经生成了一个用户信息表,下面要做的就是设计用户登录功能了。 打开mysite4/login/views.py 文件 #coding=utf-8 from django.shortcuts import render,render_to_response from django.http import HttpResponseRedirect from login.models import User from django import forms #定义表单模型 class UserForm(forms.Form): username = forms.CharField(label='用户名:',max_length=100) password = forms.CharField(label='密码:',widget=forms.PasswordInput()) #登录 def login(request): if request.method == 'POST': uf = UserForm(request.POST) if uf.is_valid(): #获取表单用户密码 username = uf.cleaned_data['username'] password = uf.cleaned_data['password'] #获取的表单数据与数据库进行比较 user = User.objects.filter(username__exact = username,password__exact = password) if user: return render_to_response('success.html',{'username':username}) else: return HttpResponseRedirect('/login/') else: uf = UserForm() return render_to_response('login.html',{'uf':uf}) 上面登录的核心是比较,拿到用户填写的表单数据(用户名、密码)与数据库User表中的字段进行比较,根据比较结果,如果成功跳转到success.html页面,如果失败还留在原来页面login.html 。 创建模板 根据视图层的要求,我们需要创建两个页面,success.html 和login.html 先在mysite4/login/目录下创建templates目录,接着在mysite4/login/templates/目录下创建login.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>登录</title> </head> <style type="text/css"> body{color:#efd;background:#453;padding:0 5em;margin:0} h1{padding:2em 1em;background:#675} h2{color:#bf8;border-top:1px dotted #fff;margin-top:2em} p{margin:1em 0} </style> <body> <h1>登录页面:</h1> <form method = 'post' enctype="multipart/form-data"> {{uf.as_p}} <input type="submit" value = "ok" /> </form> </body> </html> 在mysite4/login/templates/目录下创建success.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title></title> </head> <body> <h1>恭喜{{username}},登录成功!</h1> </form> </body> </html> 设置模板路径 打开mysite4/mysite4/settings.py文件,在底部添加: #template TEMPLATE_DIRS=( '/home/fnngj/djpy/mysite4/login/templates') 配置URL 打开mysite4/mysite4/urls.py: from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite5.views.home', name='home'), url(r'^login/', include('login.urls')), url(r'^admin/', include(admin.site.urls)), ) 创建文件mysite4/login/urls.py from django.conf.urls import patterns, url from login import views urlpatterns = patterns('', url(r'^$', views.login, name='login'), ) 启动服务(python manage.py runserver), 访问登录页面(http://127.0.0.1:8000/login/) 用户名、密码正确,点击OK ,跳转到登录成功页面:
前言 对于web开来说,用户登陆、注册、文件上传等是最基础的功能,针对不同的web框架,相关的文章非常多,但搜索之后发现大多都不具有完整性,对于想学习web开发的新手来说就没办法一步一步的操作练习;对于web应用来说,包括数据库的创建,前端页面的开发,以及中间逻辑层的处理三部分。 本系列以可操作性为主,介绍如何通过django web框架来实现一些简单的功能。每一章都具有完整性和独立性。使用新手在动手做的过程中体会web开发的过程,过程中细节请参考相关文档。 本操作的环境: =================== deepin linux 2013(基于ubuntu) python 2.7 Django 1.6.2 =================== 创建项目与应用 #创建项目 fnngj@fnngj-H24X:~/djpy$ django-admin.py startproject mysite2 fnngj@fnngj-H24X:~/djpy$ cd mysite2 #在项目下创建一个disk应用 fnngj@fnngj-H24X:~/djpy/mysite2$ python manage.py startapp disk 目录结构如下: 打开mysite2/mysite2/settings.py文件,将disk应用添加进去: # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'disk', ) 设计Model(数据库) 打开mysite2/disk/models.py文件,添加如下内容 from django.db import models # Create your models here. class User(models.Model): username = models.CharField(max_length = 30) headImg = models.FileField(upload_to = './upload/') def __unicode__(self): return self.username 创建两个字段,username 用户存放用户名,headImg 用户存放上传文件的路径。 下面进行数据库的同步 fnngj@fnngj-H24X:~/djpy/mysite2$ python manage.py syncdb Creating tables ... Creating table django_admin_log Creating table auth_permission Creating table auth_group_permissions Creating table auth_group Creating table auth_user_groups Creating table auth_user_user_permissions Creating table auth_user Creating table django_content_type Creating table django_session Creating table disk_user You just installed Django's auth system, which means you don't have any superusers defined. Would you like to create one now? (yes/no): yes 输入yes/no Username (leave blank to use 'fnngj'): 用户名(默认当前系统用户名) Email address: fnngj@126.com 邮箱地址 Password: 密码 Password (again): 确认密码 Superuser created successfully. Installing custom SQL ... Installing indexes ... Installed 0 object(s) from 0 fixture(s) 最后生成的 disk_user 表就我是我们models.py 中所创建的类。Django 提供了他们之间的对应关系。 创建视图 1、打开mysite2/disk/views.py 文件 from django.shortcuts import render,render_to_response # Create your views here. def register(request): return render_to_response('register.html',{}) 2、创建注册页面 先在mysite2/disk/目录下创建templates目录,接着在mysite2/disk/templates/目录下创建register.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title></title> </head> <body> <h1>register</h1> </body> </html> 3、设置模板路径 打开mysite2/mysite2/settings.py文件,在底部添加: #template TEMPLATE_DIRS=( '/home/fnngj/djpy/mysite2/disk/templates' ) 4、设置URL from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'mysite2.views.home', name='home'), # url(r'^blog/', include('blog.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^disk/', 'disk.views.register'), ) 5、启动服务 fnngj@fnngj-H24X:~/djpy/mysite2$ python manage.py runserver Validating models... errors found May 20, 2014 - 13:49:21 Django version 1.6.2, using settings 'mysite2.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. 6、访问http://127.0.0.1:8000/disk/ 注册页面可以正常打开说明整个过程已经走通。这也是Django开发的基本套路。读者一定要熟练理解这个基本套路。 完善表单提交 通过上面的过程,我们只是把过程串了起来,细心你一定发现,我们的register.html 文件,并没有创建用户提交的表单,views.py文件中也并没有对用户提交的信息做处理。下面我们就针对这两个文件进一步的补充。 打开mysite2/disk/templates/register.html 文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title></title> </head> <body> <h1>register</h1> <form method="post" enctype="multipart/form-data" > {{uf.as_p}} <input type="submit" value="ok"/> </form> </body> </html> 打开mysite2/disk/views.py 文件: from django.shortcuts import render,render_to_response from django import forms from django.http import HttpResponse # Create your views here. class UserForm(forms.Form): username = forms.CharField() headImg = forms.FileField() def register(request): if request.method == "POST": uf = UserForm(request.POST,request.FILES) if uf.is_valid(): return HttpResponse('upload ok!') else: uf = UserForm() return render_to_response('register.html',{'uf':uf}) 再次刷新http://127.0.0.1:8000/disk/ 页面 填写用户名,选择本地上传文件,点击“ok” 抛出一个错误,这个错误比较友好,所以不是我们操作过程中的小错误。 打开mysite2/mysite2/settings.py文件,将下面一行代码注释: MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', #'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) 再次刷新http://127.0.0.1:8000/disk/ 页面,我们就可以正常将用户名和文件提交了! 将数据写入数据库 虽然已经实现了数据的提交,但用户名与文件并没有真正的写入到数据库。我们来进一步的完善mysite2/disk/views.py 文件: #coding=utf-8 from django.shortcuts import render,render_to_response from django import forms from django.http import HttpResponse from disk.models import User # Create your views here. class UserForm(forms.Form): username = forms.CharField() headImg = forms.FileField() def register(request): if request.method == "POST": uf = UserForm(request.POST,request.FILES) if uf.is_valid(): #获取表单信息 username = uf.cleaned_data['username'] headImg = uf.cleaned_data['headImg'] #写入数据库 user = User() user.username = username user.headImg = headImg user.save() return HttpResponse('upload ok!') else: uf = UserForm() return render_to_response('register.html',{'uf':uf}) 再次刷新http://127.0.0.1:8000/disk/ 页面,完成文件的上传。 在项目的目录下,我们居然发现了用户提交的文件。 那数据库中保存的是什么呢? fnngj@fnngj-H24X:~/djpy/mysite2$ sqlite3 db.sqlite3 SQLite version 3.7.15.2 2013-01-09 11:53:05 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite> select * from disk_user; | Alen | upload/desk.jpg sqlite> 通过查看数据库发现,我们数据库中存放的并非用户上传的文件本身,而是文件的存放路径。 OK ,你可以在此基础上继续扩展,例如用户提交成功后,将用户名上传的文件名显示出来,或为上传页面加一个漂亮的样式等。
文章还可在我的github上找到,排版更友好一点:grunt从入门到自定义项目模板 一.Grunt入门介绍 1. Grunt是神马 基于任务的命令行构建工具(针对JavaScript项目) 链接:http://gruntjs.com/ 2. 使用Grunt的理由 前端的工具算得上是五花八门,在介绍如何Grunt之前,首先我们得反问自己: Grunt能够帮我们解决什么问题? 是否有其他更合适的替代方案? 3. Grunt能够帮我们解决什么问题? 作为一名开发人员,我们见过了不少功能胡里花哨但并不实用的工具。但是,我们很少会因为一个工具功能很强大而去使用它。更多地,是因为在工作中我们遇到了一些问题,而某个工具刚好帮我们解决了这些问题。 假设我们有个叫IMWEB_PROJ的项目,该项目主要包含两个功能模块,分别是moduleA、moduleB。回想一下,作为一名前端开发人员,从功能开发到产品正式上线,我们的工作流程是什么样的: 正式进入编码工作前,得做些准备工作: 新建目录IMWEB_PROJ,index.html为主入口;根目录下面再另外新建三个目录js、css、img,分别用来存放js文件、css文件、图片 IMWEB_PROJ/js下新建个main.js作为项目主逻辑的入口,添加moduleA.js、modueB.js,对了,不能把我们的基础组件Badjs.js、simple.js、nohost.js给忘了 IMWEB_PROJ/css下新建个reset.css,添加moduleA.css、moduleB.css 热火朝天地编码,产品终于即将上线,上线前的准备工作同样不能马虎 JSHint——检查下JS代码规范性,避免进行类似隐式全局变量这样的坑里 concat——JS文件合并,合理减少请求数,提升加载速度 cssmin——CSS文件合并,合理减少请求数,提升加载速度 Uglyfy——压缩文件,减少文件尺寸,提升用户侧加载速度 QUnit——单元测试,提高项目可维护性,结合递归测试可尽早发现潜在问题 … 上面的场景是不是很眼熟?重复而枯燥的工作占据了我们太多的时间,忘了谁说过,当重复做一件事超过三次,就应该考虑将它自动化。 Grunt正是为了解决上述问题而诞生,它将上面提到的项目结构生成、JSHint检查、文件合并、压缩、单元测试等繁琐的工作变成一个个可自动化完成的任务,一键搞定。 4. 其他使用Grunt的理由 文档丰富:详细的使用说明,从入门使用,到高级定制,非常详尽 插件丰富:基本能够想到的常用的任务,都可以找到 社区活跃:Grunt的开发团队还是挺勤劳的,社区活跃度也挺高 5. 是否有其他更合适的替代方案? 当然有,而且不少,Ant、Yeoman、Mod、Fiddler+willow+qzmin等,先不展开 二. 从零开始使用Grunt 参考链接:http://gruntjs.com/getting-started Grunt使用场景通常分两种: 维护现有的Grunt项目——已经配置好的项目,下面以jQuery Plugin项目为例进行讲解,简单了解下一个Grunt项目的基本结构; 新创建的Grunt项目——包括项目目录结构的创建,到Grunt任务的配置等;这里可以采用现有的Grunt模板,也可以采用自定义的模板;下文会采用自定义模板的形式,逐步讲解如何创建一个**IMWEB团队的前端基础项目结构** 1. 环境以及依赖 Grunt以及Grunt的插件,都是通过npm进行安装和管理,所以首先得安装node环境,不赘述,见 http://nodejs.org/ 2. 关于版本 注意:为了解决多版本并存的问题,从0.4.x版本开始,每个项目需独立安装Grunt及对应插件,版本分别如下: Grunt 0.4.x Nodejs >=0.8.0 3. 卸载老版本Grunt(版本<0.4.0) grunt从版本0.3.X到0.4.x,变化比较大,主要是为了解决Grunt多版本共存的问题,有兴趣的童鞋可以了解下。如果之前安装了0.3.x版本,需先进行卸载 npm uninstall -g grunt 4. 安装grunt-cli grunt-cli的主要作用是让我们可以运行Grunt命令,加上-g,则可以在任意目录下运行,不展开 npm install -g grunt-cli 5. 安装grunt-init grunt-init是个脚手架工具,它可以帮你完成项目的自动化创建,包括项目的目录结构,每个目录里的文件等。具体情况要看你运行grunt-init指定的模板,以及创建过程中你对问题的回答,下文会简单讲到这个功能。 先运行下面命令安装grunt-init, npm install -g grunt-init 下面我们先通过安装jQuery Plugin模板,来展示Gurnt模板的安装,项目的创建,以及一个Grunt项目的目录结构 三、jQuery Plugin示例:如何通过现有模板创建项目、运行Grunt任务 参考连接:http://gruntjs.com/project-scaffolding 1. 安装jQuery Plugin模板 下面命令可以查看官方维护的Grunt模板 grunt-init --help 运行下面命令安装jQuery模板 git clone git@github.com:gruntjs/grunt-init-jquery.git ~/.grunt-init/jquery 2. 根据jQuery Plugin模板创建项目 在上一步中我们已经安装好了jQuery模板,接着运行下面命令,安装jQuery项目 grunt-init jquery 按照引导回答下面问题,完成项目的创建 Please answer the following: [?] Project name (test) DemoJQuery [?] Project title (DemojQuery) [?] Description (The best jQuery plugin ever.) just for test [?] Version (0.1.0) 1.0.0 [?] Project git repository (git://github.com/root/test.git) [?] Project homepage (https://github.com/root/test) [?] Project issues tracker (https://github.com/root/test/issues) [?] Licenses (MIT) [?] Author name (none) 程序 猿 小卡 [?] Author email (none) [?] Author url (none) http://chyingp.cnblogs.com [?] Required jQuery version (*) 1.9.0 [?] Do you need to make any changes to the above before continuing? (y/N) N 项目目录结构如下: //项目目录结构 -rw-r--r-- 1 root staff 1670 5 9 15:13 CONTRIBUTING.md -rw-r--r-- 1 root staff 559 5 9 15:13 DemoJQuery.jquery.json -rw-r--r-- 1 root staff 2184 5 9 15:13 Gruntfile.js -rw-r--r-- 1 root staff 1053 5 9 15:13 LICENSE-MIT -rw-r--r-- 1 root staff 543 5 9 15:13 README.md drwxr-xr-x 5 root staff 170 5 9 15:13 libs -rw-r--r-- 1 root staff 423 5 9 15:13 package.json drwxr-xr-x 4 root staff 136 5 9 15:13 src drwxr-xr-x 5 root staff 170 5 9 15:13 test 从上面的目录结构,大致可以看出各个目录、文件的作用,其中我们需要注意的是两个文件Gruntfile.js、package.json,这两个文件都需要放在项目跟目录下。下面会稍微详细介绍到: Gruntfile.js 项目的Grunt配置信息,包括模块依赖、任务定义 package.json 项目node模块的依赖信息,主要根据Gruntfile生成 其他其他文件非Grunt项目必须的,可以暂时不去看它 3. 运行Grunt任务 首先运行下面命令,安装所需node模块,耐心等候安装完即可 npm install 输入下面命令,运行Grunt任务 grunt 输出如下,done Running "jshint:gruntfile" (jshint) task >> 1 file lint free. Running "jshint:src" (jshint) task >> 1 file lint free. ... 4. 如何创建package.json 方式一:运行下面命令,通过逐步回答问题的方式创建基础的package.json文件2``` npm install 方式二:创建空的package.json文件,拷贝下面内容,根据需要进行修改 { "name": "HelloProj", "version": "0.1.0", "devDependencies": { "grunt": "~0.4.1", "grunt-contrib-jshint": "~0.1.1", "grunt-contrib-nodeunit": "~0.1.2" } } 创建完package.json,运行如下命令,安装所需插件 npm install 5. 如何安装Grunt 运行如下命令,安装最新版的Grunt npm install grunt --save-dev 6. 创建Gruntfile.js Gruntfile.js的配置文件格式并不复杂,不过刚开始看的时候会有些云里雾里,直接拿官方范例进行修改即可。参考链接:http://gruntjs.com/sample-gruntfile module.exports = function(grunt) { // 项目配置信息,这里只是演示用,内容随便填的 grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { //压缩文件 build: { src: 'src/<%= pkg.name %>.js', dest: 'build/<%= pkg.name %>.min.js' } }, concat: { //合并文件 js:{ src: ['js/moduleA.js', 'js/moduleB.js'], dest: 'dist/js/moduleA-moduleB.js' }, css:{ src:['dist/css/moduleA.css', 'dist/css/moduleB.css'], dest: 'dist/css/moduleB.css' } } }); // 加载uglify插件,完成压缩任务 grunt.loadNpmTasks('grunt-contrib-uglify'); // 加载concat插件,完成文件合并任务 grunt.loadNpmTasks('grunt-contrib-concat'); // 默认任务,如果运行grunt命令,且后面没有指定任务,或为defalut时,运行这个 grunt.registerTask('default', ['concat', 'uglify']); }; 其实这种方式还是有点麻烦,Grunt团队还是比较人性化的,针对Gruntfile,还提供了一个单独的plugin,让我们免去重复劳动之苦,后面再讲 四. imweb_template:自定义模板,创建IMWEB团队专属的前端项目骨架 1. 下载Grunt官方示例模板 下载链接:https://github.com/gruntjs/grunt-init-jquery 打开下载下来的示例目录,可以看到如下内容: -rwxr-xr-x@ 1 casperchen staff 877 2 18 09:00 README.md -rwxr-xr-x@ 1 casperchen staff 138 2 18 09:00 rename.json drwxr-xr-x@ 10 casperchen staff 340 2 18 09:00 root -rwxr-xr-x@ 1 casperchen staff 3521 2 18 09:00 template.js 简单介绍下里面内容: template.js 主模板文件,非常重要!里面主要内容有:项目创建时需要回答的问题,项目依赖的Grunt模块(根据这个生成package.json) rename.json 针对当前模板的目录/文 件重命名规则,不赘述 root/ 重要!在这个目录里的文件,通过该模板生成项目结构时,会将root目录下的文件都拷贝到项目中去 2. 创建自定义项目之前 将之前下载的grunt-init-jquery-master重命名为imweb_template,然后就开始我们的模板自定义之旅了!鉴于这块的内容实在太多,就不详细讲解,直接贴上修改后的文件,可以更为直观,如需深入了解,可查看相关链接:http://gruntjs.com/project-scaffolding 3. 修改imweb_template/template.js 下面是template.js最常包含的一些内容,主要包括: exports.description 模板简单介绍信息 exports.notes 开始回答项目相关问题前,控制台打印的相关信息 exports.after 开始回答项目相关问题前,控制台打印的相关信息 init.process 项目创建的时候,需要回答的问题 init.writePackageJSON 生成package.json,供Grunt、npm使用 /* * 模板名字 * https://gruntjs.com/ * * 版权信息 * Licensed under the MIT license. */ 'use strict'; // 模板简单介绍信息 exports.description = '创建IMWEB专属模板,带文件合并压缩哦!'; // 开始回答项目相关问题前,控制台打印的相关信息 exports.notes = '这段信息出现位置:回答各种项目相关的信息之前 ' + '\n\n'+ '逐个填写就行,如果不想填的会可以直接enter跳过'; // 结束回答项目相关问题后,控制台打印出来的信息 exports.after = '项目主框架已经搭建好了,现在可以运行 ' + '\n\n' + '1、npm install 安装项目依赖的node模块\n'+ '2、grunt 运行任务,包括文件压缩、合并、校验等\n\n'; // 如果运行grunt-init运行的那个目录下,有目录或文件符合warOn指定的模式 // 则会跑出警告,防止用户不小心把当前目录下的文件覆盖了,一般都为*,如果要强制运行,可加上--force // 例:grunt-init --force imweb_template exports.warnOn = '*'; // The actual init template. exports.template = function(grunt, init, done) { init.process({type: 'IMWEB'}, [ // 项目创建的时候,需要回答的问题 init.prompt('name'), init.prompt('title'), init.prompt('description', 'IMWEB项目骨架'), init.prompt('version', '1.0.0'), init.prompt('author_name'), init.prompt('author_email'), ], function(err, props) { props.keywords = []; // 需要拷贝处理的文件,这句一般不用改它 var files = init.filesToCopy(props); // 实际修改跟处理的文件,noProcess表示不进行处理 init.copyAndProcess(files, props, {noProcess: 'libs/**'}); // 生成package.json,供Grunt、npm使用 init.writePackageJSON('package.json', { name: 'IMWEB-PROJ', version: '0.0.0-ignored', npm_test: 'grunt qunit', node_version: '>= 0.8.0', devDependencies: { 'grunt-contrib-jshint': '~0.1.1', 'grunt-contrib-qunit': '~0.1.1', 'grunt-contrib-concat': '~0.1.2', 'grunt-contrib-uglify': '~0.1.1', 'grunt-contrib-cssmin': '~0.6.0', 'grunt-contrib-watch': '~0.2.0', 'grunt-contrib-clean': '~0.4.0', }, }); // All done! done(); }); }; 4. 修改imweb_template/rename.json reame.json的作用比较简单,定义了从root目录将文件拷贝到实际项目下时的路径映射关系,以sourcepath: destpath的形式声明。sourcepath是相对于root的路径,而destpath则是相对于实际项目的路径。 ps:当destpath为false时,sourcepath对应的文件不会被拷贝到项目中去 { "src/*.js": "js/*.js", "test/test.html": "test/test.html" } 5. imweb_template/root 目录 进入root目录,可以看到很多文件,其中我们需要关注的有Gruntfile.js、README.md Gruntfile.js 项目的任务配置信息,把基础任务,如jshint、concat、uglify等配置好即可,其他的各个任务可自行扩充 README.md 项目的readme信息,一个调理清晰的readme很重要 -rwxr-xr-x@ 1 casperchen staff 2408 5 10 09:34 Gruntfile.js -rwxr-xr-x@ 1 casperchen staff 605 2 18 09:00 README.md drwxr-xr-x 4 casperchen staff 136 5 9 20:31 css drwxr-xr-x@ 8 casperchen staff 272 5 9 20:44 js drwxr-xr-x@ 5 casperchen staff 170 2 18 09:00 libs drwxr-xr-x@ 5 casperchen staff 170 2 18 09:00 test 6. 修改Gruntfile.js 对Gruntfile.js文件进行修改,如下,熟悉qzmin配置文件的童鞋应该很容易看懂 'use strict'; module.exports = function(grunt) { // Project configuration. grunt.initConfig({ // Metadata. pkg: grunt.file.readJSON('package.json'), banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + ' */\n', // 任务配置信息 clean: { // Grunt任务开始前的清理工作 files: ['dist'] }, concat: { //文件压缩 js_and_css: { files: { // js文件合并 'dist/js/base.js': ['js/simple.js', 'js/badjs.js', 'js/nohost.js'], 'dist/js/main.js': ['js/moduleA.js', 'js/moduleB.js' 'js/main.js'], // css文件合并 'dist/css/style.css': ['css/reset.css', 'css/moduleA.css', 'css/moduleB.css'] } } }, uglify: { //js文件压缩 js: { files: { 'dist/js/base.min.js': ['dist/js/base.js'], 'dist/js/main.min.js': ['dist/js/main.js'] } } }, cssmin:{ //CSS文件压缩 css: { files: { 'dist/css/style.min.css': ['dist/css/style.css'] } } }, qunit: { //单元测试,范例中未启用 files: ['test/**/*.html'] }, jshint: { //文件校验,范例中未启用 gruntfile: { options: { jshintrc: '.jshintrc' }, src: 'Gruntfile.js' }, src: { options: { jshintrc: 'js/.jshintrc' }, src: ['js/**/*.js'] }, test: { options: { jshintrc: 'test/.jshintrc' }, src: ['test/**/*.js'] } }, watch: { //watch任务,实时监听文件的变化,并进行编译 gruntfile: { files: '<%= jshint.gruntfile.src %>', tasks: ['jshint:gruntfile'] }, src: { files: '<%= jshint.src.src %>', tasks: ['jshint:src', 'qunit'] }, test: { files: '<%= jshint.test.src %>', tasks: ['jshint:test', 'qunit'] } }, }); // 加载各种grunt插件完成任务 grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); // 默认任务 grunt.registerTask('default', ['clean', 'concat', 'uglify', 'cssmin']); //grunt.registerTask('default', ['jshint', 'qunit', 'clean', 'concat', 'uglify']); }; 7. 进入实战 花了一点时间把IMWEB_PROJ配置好,现在终于到了实际运作阶段了。假设我们当前在目录IMWEB_PROJ下,imweb_template为IMWEB_PROJ目录当前仅有的内容 drwxr-xr-x@ 8 casperchen staff 272 5 10 00:59 imweb_template 操作步骤可参照**jQuery Plugin示例:如何通过现有模板创建项目、运行Grunt任务**,下面直接上命令 grunt-init --force imweb_template/ npm install grunt 下面为运行grunt命令后控制台输出的信息 Running "clean:files" (clean) task Cleaning "dist"...OK Running "concat:js_and_css" (concat) task File "dist/js/base.js" created. File "dist/js/main.js" created. File "dist/css/style.css" created. Running "uglify:js" (uglify) task File "dist/js/base.min.js" created. Uncompressed size: 96927 bytes. Compressed size: 7609 bytes gzipped (34814 bytes minified). File "dist/js/main.min.js" created. Uncompressed size: 926 bytes. Compressed size: 93 bytes gzipped (305 bytes minified). Running "cssmin:css" (cssmin) task File dist/css/style.min.css created. Done, without errors. 可以看到HelloProj目录下的内容发生了改变,enjoy yourself! -rw-r--r-- 1 root staff 2398 5 10 14:39 Gruntfile.js -rw-r--r-- 1 root staff 605 5 10 14:37 README.md drwxr-xr-x 6 root staff 204 5 10 14:37 css drwxr-xr-x 4 root staff 136 5 10 14:39 dist drwxr-xr-x@ 8 casperchen staff 272 5 10 00:59 imweb_template drwxr-xr-x 10 root staff 340 5 10 14:37 js drwxr-xr-x 5 root staff 170 5 9 20:17 libs drwxr-xr-x 10 casperchen staff 340 5 10 09:28 node_modules -rw-r--r-- 1 root staff 458 5 10 14:37 package.json drwxr-xr-x 4 root staff 136 5 9 20:17 src drwxr-xr-x 5 root staff 170 5 9 20:17 test 五. 关于Grunt、Ant、Mod的对比 上面对Grunt进行了入门介绍,下面简单说下Ant、aven Ant:做过java开发的童鞋一般都不会陌生,功能很强大,相对Grunt来说更容易入门,配置文件更加友好,据说yahoo前端团队用的Ant,推荐个不错的教程:http://www.book.36ria.com/ant/index.html#index Mod:元彦童鞋开发维护,功能很强大,grunt能完成的,Mod都能完成,而且使用更加贴近我们的项目实践,入门更简单(有部分原因是因为 mod集成了很多常用户任务,而Grunt早期也是这么做的,不过因为多版本的问题放弃了这种做法),之前听过元彦的分享,挺不错的,打算在项目中试用 下。github地址:https://github.com/modulejs/modjs 六. 写在最后 由于时间问题,这里没有对Grunt、Ant、Mod进行详细的对比,来个todo吧~~
写在前面:本文比较基础,仅是一枚菜鸟接触jquery过程中的一点思考和总结,内容较基础,希望能对刚接触jQuery的童鞋有一点帮助 :) 按照国际惯例(其实就是俺写作的习惯),首先抛出待问题的场景。至于问题的答案,文章并不会急着揭晓,而是通过逐层递进的方式,展现思考、解决一个问题的过程 1、如何给一个id为casper的标签添加一个名为“world”的class 考虑下面一个场景,假设我们页面上有个id为casper的div标签,如下所示 <div id="casper" class="hello">casper是个大傻瓜,啦啦啦啦啦</div> 现在我们想要给它添加一个class,比如“world”,用jquery的话如何实现?很简单,不卖关子 $('#casper').addClass('world'); 很好,接下来我们思考:如何不用jquery,我们如何如何实现实现上述功能?最简单的方式: var node = document.getElementById('casper'); node.className += ' world'; getElementById、getElementsByTagName神马的,名字老长老长的,写着有点不爽,于是把getElementById这个方法用美元($)包装下: function $(id){ return document.getElementById(id); } $('casper').className += ' world'; className品字符串神马的,jquery的调用方式相比麻烦多了,那再改进下: function $(id){ var node = document.getElementById(id); node.addClass = function(addName){ node.className += ' ' + addName; }; return document.getElementById(id); } $('casper').addClass('world'); 看上去挺像那么一回事了,多优雅的接口啊(热泪盈眶中)~ 真的是这样吗,再仔细瞧瞧?于是果断发现不对劲的地方:对于$,每次调用,都会给返回的dom元素上添加一个addClass方法,这对空间来说是极大的浪费。当然,可以将addClass方法抽取出来: function addClass(className){ //实现略 } function $(id){ var node = document.getElementById(id); node.addClass = addClass; return document.getElementById(id); } $('casper').addClass('world'); 原先的空间浪费问题可以在很大程度上得到解决,但明显这解决方法还不够好。如果有那么一种实现方式,让所有的对象实例都共享一个方法。。。 2、jQuery中的实现思路 同样不必卖关子,这里说的就是原型方法,我们再看下jquery的调用方式 $('#casper').addClass('world'); $('#casper')并不是像我们上面那样,简单地将id为casper的元素返回。实际上,$('#casper')返回的是一个jQuery对象,该对象特征如下: 拥有一个length属性,length等于你调用$选中的元素的数目,在$('#casper')中为1 拥有0~n-1的实例属性,分别对应调用$时选中的第1~第n个元素,如本例中$('#casper')[0]即为目标dom元素 拥有一堆原型方法,如常见的addClass、removeClass、bind等 根据上面三点,很容易对我们之前写的代码进行修改,如下: function $(id){ this[0] = document.getElementById(id); this.length = 1; } $.prototype.addClass = function(className){ this[0].className += ' ' + className; }; var noode = new $('casper'); node.addClass('world'); 其实就几行代码的事情,但。。。还是觉得有些不对劲,new $('casper'),平常在用jquery的时候似乎不需要new一下的说,想想看,我们代码中一坨new是多么可怕的事情~ 好吧,其实是因为jQuery帮你完成了构造函数调用的这部分工作,这一小小的细节改善对jQuery的流行起到了很大的帮助。按照这个思路,继续修改之前的代码: function $(id){ if(!(this instanceof $) return new $(id); //加了这么个语句 this[0] = document.getElementById(id); this.length = 1; } //其他一样,节省空间不贴代码 在上面的代码中,只有一点小小的修改,就是加了个判断语句 if(!(this instanceof $)) ,作用在于判断,当$被调用时,究竟是采用以下两种调用方式的哪一种,关于这种判断方式,可参考之前写的《【经验总结】构造函数的强制调用》: $('casper'),直接调用,于是this为window new $('casper'),此时$为构造方法,this instanceof $ == true 3、jQuery中的源码实现以及问题所在(俺的疑惑) 罗嗦了这么多,我们看看关于这点,jQuery里是如何实现的,源码大致如下,一些不相干的代码略过: (function( window, undefined ) { //去掉无关变量声明等,防止干扰分析 var jQuery = (function() { // Define a local copy of jQuery var jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context, rootjQuery ); }, //一堆无关细节暂时略过 jQuery.fn = jQuery.prototype = { constructor: jQuery, init: function( selector, context, rootjQuery ) { //继续略过 } }; // Give the init function the jQuery prototype for later instantiation jQuery.fn.init.prototype = jQuery.fn; return jQuery; })(); window.jQuery = window.$ = jQuery; })( window ); 对于研究过jQuery源码或曾经打算研究jQuery源码的同学来说,上面这段代码肯定不会陌生,它有一个特点:看上去比较晦涩,特别是是结合了jQuery源码里面比较诡异的代码缩进~ 通过闭包返回的jQuery对象,闭包里面是有jQuery函数定义,jQuery函数里面return了new jQuery.fn.init 。。。快速看懂上面这段代码的秘诀在于:一个支持代码高亮和职能中括号匹配的编辑器,比如webstorm。。。 上面只是开个小玩笑,绕了这么久,无法是想做下面几件事情: 无论有没有new,只要调用$,都给你返回一个jQuery对象(实际上jQuery.fn.init才是实际的构造函数) 将jQuery.fn.init.fn指向jQuery.prototype,这样的话,当我们通过$.fn.newPrototypeAttr 方式向jQuery添加原型属性或方法,其实最终都成为了jQuery.fn.init地原型属性或方法 将constructor属性指向jQuery,不然$('#casper').constructor 获得的会是jQuery.fn.init 个人觉得上面这段代码有些费解,似乎完全可以采用相对不那么曲折的方式实现,如下所示,其实思路都是相同的: 然后,就是添加各种原型方法了,兼容性处理和优雅的API,这块才是精华,这里还没讲到。 (function(){ var jQuery = function(id){ return new _jquery(id); }; var _jquery = function(id){ //此处各种选择分支神马的都忽略~ this[0] = document.getElementById(id); this.length = 1; }; jQuery.fn = jQuery.prototype = { constructor: jQuery, addClass: function(className){ this[0].className += ' ' + className; } }; _jquery.prototype = jQuery.fn; window.$ = window.jQuery = jQuery; })(); 问题:jQuery源码的那种实现方式,至今不明白作用在哪?是有其他的考虑??知道的筒子往不吝赐教! 写在后面: 文中示例如有错漏,请指出;如觉得文章对您有用,可点击“推荐” :)
如果本文看不懂的,去看的我视频吧! http://www.testpub.cn/ ------------------------------------------- Django 自称是“最适合开发有限期的完美WEB框架”。本文参考《Django web开发指南》,快速搭建一个 blog 出来,在中间涉及诸多知识点,这里不会详细说明,如果你是第一次接触Django ,本文会让你在感性上对Django有个认识,完成本文操作 后会让你有兴趣阅读的相关书籍和文档。 废话少说,come on!! 本操作的环境: =================== Windows 7/10 python 2.7 Django 1.8.2 =================== 创建工程 创建mysite工程项目: D:/djpy> django-admin.py startproject mysite 工程目录结构: manage.py ----- Django项目里面的工具,通过它可以调用django shell和数据库等。 settings.py ---- 包含了项目的默认设置,包括数据库信息,调试标志以及其他一些工作的变量。 urls.py ----- 负责把URL模式映射到应用程序。 创建blog应用 在mysite目录下创建blog应用 D:/pydj> cd mysite D:/djpy/mysite$ python manage.py startapp blog 目录结构: 初始化admin后台数据库 python 自带SQLite数据库,Django支持各种主流的数据库,这里为了方便推荐使用SQLite,如果使用其它数据库请在settings.py文件中设置。 切换到mysite创建数据库: D:/djpy/mysite$ python manage.py syncdb C:\Python27\lib\site-packages\django\core\management\commands\syncdb.py:24: RemovedInDjango19Warning: The syncdb command will be removed in Django 1.9 warnings.warn("The syncdb command will be removed in Django 1.9", RemovedInDjango19Warning) Operations to perform: Synchronize unmigrated apps: staticfiles, messages Apply all migrations: admin, contenttypes, auth, sessions Synchronizing apps without migrations: Creating tables... Running deferred SQL... Installing custom SQL... Running migrations: Rendering model states... DONE Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying sessions.0001_initial... OK You have installed Django's auth system, and don't have any superusers defined. Would you like to create one now? (yes/no): yes Username (leave blank to use 'fnngj'): 用户名(默认当前系统用户名) Email address: fnngj@126.com 邮箱地址 Password: 密码 Password (again): 重复密码 Superuser created successfully. 设置admin应用 admin 是Django 自带的一个后台管理系统。 1、添加blog应用,打开mysite/mysite/settings.py 文件: # Application definition INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', ) 在列表末尾,添加blog 应用 2、在我们创建django项目时,admin就已经创建,打开mysite/mysite/urls.py文件: from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ url(r'^admin/', include(admin.site.urls)), ] 3、启动django容器 D:\pydj\mysite>python manage.py runserver Performing system checks... System check identified no issues (0 silenced). October 04, 2015 - 20:56:45 Django version 1.8.2, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CTRL-BREAK. 4、访问后台应用 http://127.0.0.1:8000/admin 输入用户、密码,用户名密码为第一次创建数据库时创建的。回想“设置数据库”时的设置。 设计Model(即设计数据库表) 1、设计model 现在我们打开blog目录下的models.py文件,这是我们定义blog数据结构的地方。打开mysite/blog/models.py 文件进行修改: from django.db import models from django.contrib import admin # Create your models here. class BlogsPost(models.Model): title = models.CharField(max_length = 150) body = models.TextField() timestamp = models.DateTimeField() admin.site.register(BlogsPost) 2、 再次初始化数据库 D:\pydj\mysite>python manage.py makemigrations blog Migrations for 'blog': 0001_initial.py: - Create model BlogsPost D:\pydj\mysite>python manage.py syncdb C:\Python27\lib\site-packages\django\core\management\commands\syncdb.py:24: RemovedInDjango19Warning: The syncdb command will be removed in Django 1.9 warnings.warn("The syncdb command will be removed in Django 1.9", RemovedInDjango19Warning) Operations to perform: Synchronize unmigrated apps: staticfiles, messages Apply all migrations: admin, blog, contenttypes, auth, sessions Synchronizing apps without migrations: Creating tables... Running deferred SQL... Installing custom SQL... Running migrations: Rendering model states... DONE Applying blog.0001_initial... OK 3、再次runserver启动服务,访问admin后台,创建文章。 登陆成功选择add 创建博客 输入博客标题,正文、日期时间、点击save 创建博客。 设置admin 的BlogsPost界面 打开mysite/blog/models.py 文件,做如下修改: from django.db import models from django.contrib import admin # Create your models here. class BlogsPost(models.Model): title = models.CharField(max_length = 150) body = models.TextField() timestamp = models.DateTimeField() class BlogPostAdmin(admin.ModelAdmin): list_display = ('title','timestamp') admin.site.register(BlogsPost,BlogPostAdmin) 创建BlogPostAdmin类,继承admin.ModelAdmin父类,以列表的形式显示BlogPost的标题和时间。 创建blog的公共部分 从Django的角度看,一个页面具有三个典型的组件: 一个模板(template):模板负责把传递进来的信息显示出来。 一个视图(view):视图负责从数据库获取需要显示的信息。 一个URL模式:它负责把收到的请求和你的试图函数匹配,有时候也会向视图传递一些参数。 创建模板 在blog项目下创建templates目录(mysite/blog/templates/),在目录下创建模板文件index.html,内容如下: {% for post in posts %} <h2>{{ post.title }}</h2> <p>{{ post.timestamp }}</p> <p>{{ post.body }}</p> {% endfor%} 创建视图函数 打开mysite/blog/views.py文件: #coding=utf-8 from django.shortcuts import render from blog.models import BlogsPost from django.shortcuts import render_to_response # Create your views here. def index(request): blog_list = BlogsPost.objects.all() return render_to_response('index.html',{'blog_list':blog_list}) blog_list = BlogPost.objects.all() :获取数据库里面所拥有BlogPost对象 render_to_response()返回一个页面(index.html),顺带把数据库中查询出来的所有博客内容(blog_list)也一并返回。 创建blog的URL模式 在mysite/urls.py文件里添加blog的url: #coding=utf-8 from django.conf.urls import patterns, include, url from django.contrib import admin urlpatterns = patterns('', url(r'^admin/', include(admin.site.urls)), url(r'^index/$', 'blog.views.index'), ) 再次启动服务($ python manage.py runserver),访问blog应用(http://127.0.0.1:8000/blog/)。 页面如下: 当然,读者可以继续到admin后台添加blog,从而刷新这个页是否显示新添加的blog。 添加样式 创建基础模板 在mysite/blog/templates目录里创建base.html的模板: <html> <style type="text/css"> body{color:#efd;background:#453;padding:0 5em;margin:0} h1{padding:2em 1em;background:#675} h2{color:#bf8;border-top:1px dotted #fff;margin-top:2em} p{margin:1em 0} </style> <body> <h1>虫师blog</h1> <h3>大人不华,君子务实</h3> {% block content %} {% endblock %} </body> </html> 修改index.html模板,让它引用base.html模板和它的“content”块。 {% extends "base.html" %} {% block content %} {% for post in posts %} <h2>{{ post.title }}</h2> <p>{{ post.timestamp | date:"1,F jS"}}</p> <p>{{ post.body }}</p> {% endfor %} {% endblock %} 再次刷新博客页面: 请系统的学习django web框架,然后在此基础上做更多的扩展,开发自己真正的blog 。 ------------------------------------------------------------------------------------------------------------------------------------- 参考: Python Django 快速Web应用开发入门 : http://study.163.com/course/introduction/320022.htm#/courseDetail 《Django Web开发指南》 第二章 blog:http://my.oschina.net/matrixchan/blog/184445 --------------更新-------------- django更新到了1.8 ,所以,重新进行了编辑。2015.10.4
行为驱动开发(BDD),依然高大上的矗立在远方,很少被人问津,一方面是BDD的思想不太容易理解,别一方面BDD的资料并不多。中文的资料就更少了。 之前增写过一篇《python BDD 框架之lettuce》 来介绍BDD ,本文将在此基础上通过lettuce 和webdriver来实现自动化测试,感兴趣的测试同学跟我一起装X吧! 下面向读者介绍如何通过lettuce 和 webdriver 结合来编写自动化脚本。 环境配置: ------------------------------------------ 前提:有python载发环境,并且安装了pip ,强烈建议在linux下操作。 第一步,安装lettuce root@machine:~$ [sudo] pip install lettuce 第二步,安装lettuce_webdriver https://pypi.python.org/pypi/lettuce_webdriver root@machine:~$ [sudo] pip install lettuce_webdriver 第三步,安装nose nose继承自unittest,属于第三方的python单元测试框架,且更容易使用,lettuce_webdriver包的运行依赖于nose模块 https://pypi.python.org/pypi/nose/ root@machine:~$ [sudo] pip install nose --------------------------------- 下面以为百度搜索为例,好吧!谁让这个例子能简单明了的讲解问题呢。所以,我们再次以百度搜索为例。 一般的百度搜索自动化脚本是这样的: # coding = utf-8 from selenium import webdriver browser = webdriver.Firefox() browser.get("http://www.baidu.com") browser.find_element_by_id("kw1").send_keys("selenium") browser.find_element_by_id("su1").click() browser.quit() 下面看看BDD是怎么玩的: 创建目录结构如下: .../test/features/baidu.feature /step_definitions/setps.py /support/terrain.py 先来回顾一下我们去写百度搜索脚本的过程: 功能:访问百度 场景:搜索selenium 我去访问百度的“http://www.badiu.com” 通过id 为“kw1”找到输入框并输入“selenium”关键字 点击 id为“su1” 按钮 然后,我在搜索结果上找到了“seleniumhq.com”的网址 最后,我关闭了浏览器 OK,确定了我们要做事情的过程,下面将其转换成BDD 的描述文件。 baidu.feature Feature: Go to baidu Scenario: search selenium Given I go to "http://www.baidu.com/" When I fill in field with id "kw1" with "selenium" And I click id "su1" with baidu once Then I should see "seleniumhq.org" within 2 second Then I close browser setps.py from lettuce import * from lettuce_webdriver.util import assert_false from lettuce_webdriver.util import AssertContextManager def input_frame(browser, attribute): xpath = "//input[@id='%s']" % attribute elems = browser.find_elements_by_xpath(xpath) return elems[0] if elems else False def click_button(browser,attribute): xpath = "//input[@id='%s']" % attribute elems = browser.find_elements_by_xpath(xpath) return elems[0] if elems else False #定位输入框输入关键字 @step('I fill in field with id "(.*?)" with "(.*?)"') def baidu_text(step,field_name,value): with AssertContextManager(step): text_field = input_frame(world.browser, field_name) text_field.clear() text_field.send_keys(value) #点击“百度一下”按钮 @step('I click id "(.*?)" with baidu once') def baidu_click(step,field_name): with AssertContextManager(step): click_field = click_button(world.browser,field_name) click_field.click() #关闭浏览器 @step('I close browser') def close_browser(step): world.browser.quit() 注意:@step (‘xxx’)读取了baidu.feature 里有关键字,用正则表达式匹配的。 这是BDD的核心点。理解了这一点,就知道BDD是怎么玩的了。 terrain.py from lettuce import before, world from selenium import webdriver import lettuce_webdriver.webdriver @before.all def setup_browser(): world.browser = webdriver.Friefox() terrain文件配置浏览器驱动,可以作用于所有测试用例。 在.../test/目录下输入lettuce命令,运行结果如下图 对于当前测试用例来说,添加新的测试场景也非常简单,我们只用修改baidu.feature即可: Feature: Go to baidu Scenario: search selenium Given I go to "http://www.baidu.com/" When I fill in field with id "kw1" with "selenium" And I click id "su1" with baidu once Then I should see "seleniumhq.org" within 2 second Scenario: search lettuce_webdriver Given I go to "http://www.baidu.com/" When I fill in field with id "kw1" with "lettuce_webdriver" And I click id "su1" with baidu once Then I should see "pypi.python.org" within 2 second Then I close browser 运行结果如下: 通过lettuce 来编写自动化脚本将是一件非常有趣的事儿。
函数式编程是使用一系列函数去解决问题,按照一般编程思维,面对问题时我们的思考方式是“怎么干”,而函数函数式编程的思考方式是我要“干什么”。 至于函数式编程的特点暂不总结,我们直接拿例子来体会什么是函数式编程。 lambda表达式(匿名函数): 普通函数与匿名函数的定义方式: #普通函数 def add(a,b): return a + b print add(2,3) #匿名函数 add = lambda a,b : a + b print add(2,3) #========输出=========== 5 匿名函数的命名规则,用lamdba 关键字标识,冒号(:)左侧表示函数接收的参数(a,b) ,冒号(:)右侧表示函数的返回值(a+b)。 因为lamdba在创建时不需要命名,所以,叫匿名函数^_^ Map函数: 计算字符串长度 abc = ['com','fnng','cnblogs'] for i in range(len(abc)): print len(abc[i]) #========输出=========== 4 定义abc字符串数组,计算abc长度然后循环输出数组中每个字符串的长度。 来看看map()函数是如何来实现这个过程的。 abc_len = map(len,['hao','fnng','cnblogs']) print abc_len #========输出=========== [3, 4, 7] 虽然,输出的结果中是一样的,但它们的形式不同,第一种是单纯的数值了,map()函数的输出仍然保持了数组的格式。 大小写转换; python提供有了,upper() 和 lower() 来转换大小写。 #大小写转换 ss='hello WORLD!' print ss.upper() #转换成大写 print ss.lower() #转换成小写 #========输出=========== HELLO WORLD! hello world! 通过map()函数转换: def to_lower(item): return item.lower() name = map(to_lower,['cOm','FNng','cnBLoGs']) print name #========输出=========== ['com', 'fnng', 'cnblogs'] 这个例子中我们可以看到,我们写义了一个函数toUpper,这个函数没有改变传进来的值,只是把传进来的值做个简单的操作,然后返回。然后,我们把其用在map函数中,就可以很清楚地描述出我们想要干什么。 再来看看普通的方式是如何实现字符串大小写转换的: abc = ['cOm','FNng','cnBLoGs'] lowname = [] for i in range(len(abc)): lowname.append(abc[i].lower()) print lowname #========输出=========== ['hao', 'fnng', 'cnblogs'] map()函数加上lambda表达式(匿名函数)可以实现更强大的功能。 #求平方 #0*0,1*1,2*2,3*3,....8*8 squares = map(lambda x : x*x ,range(9)) print squares #========输出=========== [0, 1, 4, 9, 16, 25, 36, 49, 64] Reduce函数: def add(a,b): return a+b add = reduce(add,[2,3,4]) print add #========输出=========== 对于Reduce函数每次是需要对两个数据进行处理的,首选取2 和3 ,通过add函数相加之后得到5,接着拿5和4 ,再由add函数处理,最终得到9 。 在前面map函数例子中我们可以看到,map函数是每次只对一个数据进行处理。 然后,我们发现通过Reduce函数加lambda表达式式实现阶乘是如何简单: #5阶乘 #5!=1*2*3*4*5 print reduce(lambda x,y: x*y, range(1,6)) #========输出=========== Python中的除了map和reduce外,还有一些别的如filter, find, all, any的函数做辅助(其它函数式的语言也有),可以让你的代码更简洁,更易读。 我们再来看一个比较复杂的例子: #计算数组中正整数的值 number =[2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8] count = 0 sum = 0 for i in range(len(number)): if number[i]>0: count += 1 sum += number[i] print sum,count if count>0: average = sum/count print average #========输出=========== 6 如果用函数式编程,这个例子可以写成这样: number =[2, -5, 9, -7, 2, 5, 4, -1, 0, -3, 8] sum = filter(lambda x: x>0, number) average = reduce(lambda x,y: x+y, sum)/len(sum) print average #========输出=========== 最后我们可以看到,函数式编程有如下好处: 1)代码更简单了。 2)数据集,操作,返回值都放到了一起。 3)你在读代码的时候,没有了循环体,于是就可以少了些临时变量,以及变量倒来倒去逻辑。 4)你的代码变成了在描述你要干什么,而不是怎么去干。
关于自动化测试,经常被问到元素的定位 与 如何设计用例。 很多时间我也帮不了你解决实际的问题,只能从个人脚本谈谈如何看待这些问题。 不得不说之元素定位 虽然,本章写了十几篇文章来讲元素的定位与操作,对于碰到的一些常见功能,如何通过技巧来定位它们,但是在实际的自动化脚本开发中,不管是新手还是具有一定经验的老手,我们面临最多的问题仍然是元素的定位问题。 有时间元素定位非常简单,例如,我们只要知道这个元素有的id和name 就可以轻松的来定位到它;有时间元素的定位却非常的令人非常头疼,尽管我们用尽了所以办法,仍然无法定位到它。在这里笔者也没万能的方法来帮你解决这些实际问题。 评估自动化可行性 对于不同的web项目,所用到的前端技术也不同,有些项目会用到EXT(一个强在的js类库),有些会用到AJAX(一种创建交互式网页应用的网页开发技术),这些技术的应用无疑对于前端开发人员可以快速的生成所需要的页面,但对于UI自动化测试人员来说,增加了定位页面元素的难度。 所以,在进行项目实现UI自动化评估的时候,页面元素的定位难度也是一个评估标准,如果处处都是很难定位的元素,那么无疑会增加脚本的开发与维护的成本,得不偿失。这个时候我可以考虑将更新多的精力放在单元或接口层的自动化上。 提高技术能力 对于自动化测试人员来说,如果熟悉前端技术也会大在降低你定位难度,熟练使用XPath和CSS技术会使你的定位变得容易很多,如果精通javascript、jquery 等技术,那么使你的定位之路变得更加随心所欲。 规范前端开发 在我们尝试实施的web项目中,大多数在设计初期,前端并没考虑到需要UI层的自动化,所以,有些前端开发人员以实现功能为目的,前端页面的代码相当不规范。这个也是自动化测试定位难的重要原因。如果开发人员在设计代码的时候规范的为元素加上id 和name属性的话,那我们的定们将会变得容易很多。 很多测试人员在对项目进行学习和实施自动化测试的过程总是觉得困难重重,就是因为这些普遍的客观原因所造成的。一方面,我们要努力学好技术,克服这些困难。另一方面,我们要清楚的认识到,自动化技术的应用与实践不是一个人的战斗。一定要得到整个团队的配合与支持。 当然,站在公司的立场,不能带来收益的事情是很难得到支持的,这个就需要读者去综合评估目前的产品真的适合引入自动化么?或者目前的阶段真的迫切需要自动化么? 不得不说之用例设计 自动化测试用例如何设计,对于新手来说也是比较难理解的问题。 不少新手刚刚掌握了写脚本的能力,一上来就拿着功能测试用例一条一条的转化成自动化用例。在写的过程中,会发现诸多问题,例如,脚本中重复代码很多,一个脚本的执行结果影响到另一个脚本的执行,有些功能用例很难转化成自动化用例等。 站在用户角度设计自动化 在功能测试的时候我们一般会遵循这个原因,但是自动化测试往往可以实现更强大的功能,所以,我们在设计脚本的时候很容易违背这个原则。例如,你要获得的数据是用户不可见的,你要判断用例是否成功的信息也是用户不可见的,或者你要模拟的是用户永远不可能做的操作等。 设计简单傻瓜的用例 自动化脚本本来是很傻瓜的。记得有同学问我,百度输入 有个自动联想功能,就是在用户输入的过程中自动配置热门搜索的关键词,例如,用户输入“自”,会自动联想“自我评价”,“自行车”等。用继续输入“自 动”,会自动联想“自动化”,“自动关机”,“自动档”等。他想定位自动联想下拉列表的某个关键词,这个关键词是百度根据用户搜索热度的变化而变化的。 再比例有同学问我,下拉列表功能,我想脚本执行时随机选择某一个选项,那么如何如何去判断随机的结果呢?换句话说,你都不知道你做了什么,怎么去判断做的结果对不对? 所以,我们在设计用例时尽量考虑简单傻瓜的用例,操作步骤简单,预期结果容易判断等。 从简单开始 对于新需要自动化的项目来说,自动化测试的实施是循序 渐进的,不要一上来就设计几百条用例,而是逐步的将功能用例转成自动化用例,在转的过程中需要不断的调整测试结构。然后,再增加稳定的测试用例。然后,再 调整测试结构。随着功能的增加你的自动化测试框架也在逐渐稳定,基础测试用例也在增加。一上来就几百条用例,需求的稍微变化,用例就可能大调整,那么你很 可能每天疲惫于用例的维护。 所以,在开始自动化的时候,你可以只对登录功能写个十来条的自动化用例。从而,渐渐的考虑将更多功能自动化起来。 半自动化对于测试人员是个不错的开始,这样你可以将更多的精力花在安全测试,探索性测试,甚至是用例体验上等。不要觉得全职自动化就是多么高大上的职位。
之前讲了多线程的一篇博客,感觉讲的意犹未尽,其实,多线程非常有意思。因为我们在使用电脑的过程中无时无刻都在多进程和多线程。我们可以接着之前的例子继续讲。请先看我的上一篇博客。 python 多线程就这么简单 从上面例子中发现线程的创建是颇为麻烦的,每创建一个线程都需要创建一个tx(t1、t2、...),如果创建的线程多时候这样极其不方便。下面对通过例子进行继续改进: player.py #coding=utf-8 from time import sleep, ctime import threading def muisc(func): for i in range(2): print 'Start playing: %s! %s' %(func,ctime()) sleep(2) def move(func): for i in range(2): print 'Start playing: %s! %s' %(func,ctime()) sleep(5) def player(name): r = name.split('.')[1] if r == 'mp3': muisc(name) else: if r == 'mp4': move(name) else: print 'error: The format is not recognized!' list = ['爱情买卖.mp3','阿凡达.mp4'] threads = [] files = range(len(list)) #创建线程 for i in files: t = threading.Thread(target=player,args=(list[i],)) threads.append(t) if __name__ == '__main__': #启动线程 for i in files: threads[i].start() for i in files: threads[i].join() #主线程 print 'end:%s' %ctime() 有趣的是我们又创建了一个player()函数,这个函数用于判断播放文件的类型。如果是mp3格式的,我们将调用music()函数,如果是mp4格式的我们调用move()函数。哪果两种格式都不是那么只能告诉用户你所提供有文件我播放不了。 然后,我们创建了一个list的文件列表,注意为文件加上后缀名。然后我们用len(list) 来计算list列表有多少个文件,这是为了帮助我们确定循环次数。 接着我们通过一个for循环,把list中的文件添加到线程中数组threads[]中。接着启动threads[]线程组,最后打印结束时间。 split()可以将一个字符串拆分成两部分,然后取其中的一部分。 >>> x = 'testing.py' >>> s = x.split('.')[1] >>> if s=='py': print s py 运行结果: Start playing: 爱情买卖.mp3! Mon Apr 21 12:48:40 2014 Start playing: 阿凡达.mp4! Mon Apr 21 12:48:40 2014 Start playing: 爱情买卖.mp3! Mon Apr 21 12:48:42 2014 Start playing: 阿凡达.mp4! Mon Apr 21 12:48:45 2014 end:Mon Apr 21 12:48:50 2014 现在向list数组中添加一个文件,程序运行时会自动为其创建一个线程。 继续改进例子: 通过上面的程序,我们发现player()用于判断文件扩展名,然后调用music()和move() ,其实,music()和move()完整工作是相同的,我们为什么不做一台超级播放器呢,不管什么文件都可以播放。经过改造,我的超级播放器诞生了。 super_player.py #coding=utf-8 from time import sleep, ctime import threading def super_player(file,time): for i in range(2): print 'Start playing: %s! %s' %(file,ctime()) sleep(time) #播放的文件与播放时长 list = {'爱情买卖.mp3':3,'阿凡达.mp4':5,'我和你.mp3':4} threads = [] files = range(len(list)) #创建线程 for file,time in list.items(): t = threading.Thread(target=super_player,args=(file,time)) threads.append(t) if __name__ == '__main__': #启动线程 for i in files: threads[i].start() for i in files: threads[i].join() #主线程 print 'end:%s' %ctime() 首先创建字典list ,用于定义要播放的文件及时长(秒),通过字典的items()方法来循环的取file和time,取到的这两个值用于创建线程。 接着创建super_player()函数,用于接收file和time,用于确定要播放的文件及时长。 最后是线程启动运行。运行结果: Start playing: 爱情买卖.mp3! Fri Apr 25 09:45:09 2014 Start playing: 我和你.mp3! Fri Apr 25 09:45:09 2014 Start playing: 阿凡达.mp4! Fri Apr 25 09:45:09 2014 Start playing: 爱情买卖.mp3! Fri Apr 25 09:45:12 2014 Start playing: 我和你.mp3! Fri Apr 25 09:45:13 2014 Start playing: 阿凡达.mp4! Fri Apr 25 09:45:14 2014 end:Fri Apr 25 09:45:19 2014 创建自己的多线程类 #coding=utf-8 import threading from time import sleep, ctime class MyThread(threading.Thread): def __init__(self,func,args,name=''): threading.Thread.__init__(self) self.name=name self.func=func self.args=args def run(self): apply(self.func,self.args) def super_play(file,time): for i in range(2): print 'Start playing: %s! %s' %(file,ctime()) sleep(time) list = {'爱情买卖.mp3':3,'阿凡达.mp4':5} #创建线程 threads = [] files = range(len(list)) for k,v in list.items(): t = MyThread(super_play,(k,v),super_play.__name__) threads.append(t) if __name__ == '__main__': #启动线程 for i in files: threads[i].start() for i in files: threads[i].join() #主线程 print 'end:%s' %ctime() MyThread(threading.Thread) 创建MyThread类,用于继承threading.Thread类。 __init__() 使用类的初始化方法对func、args、name等参数进行初始化。 apply() apply(func [, args [, kwargs ]]) 函数用于当函数参数已经存在于一个元组或字典中时,间接地调用函数。args是一个包含将要提供给函数的按位置传递的参数的元组。如果省略了args,任何参数都不会被传递,kwargs是一个包含关键字参数的字典。 apply() 用法: #不带参数的方法 >>> def say(): print 'say in' >>> apply(say) say in #函数只带元组的参数 >>> def say(a,b): print a,b >>> apply(say,('hello','虫师')) hello 虫师 #函数带关键字参数 >>> def say(a=1,b=2): print a,b >>> def haha(**kw): apply(say,(),kw) >>> haha(a='a',b='b') a b MyThread(super_play,(k,v),super_play.__name__) 由于MyThread类继承threading.Thread类,所以,我们可以使用MyThread类来创建线程。 运行结果: Start playing: 爱情买卖.mp3! Fri Apr 25 10:36:19 2014 Start playing: 阿凡达.mp4! Fri Apr 25 10:36:19 2014 Start playing: 爱情买卖.mp3! Fri Apr 25 10:36:22 2014 Start playing: 阿凡达.mp4! Fri Apr 25 10:36:24 2014 all end: Fri Apr 25 10:36:29 2014
多线程和多进程是什么自行google补脑 对于python 多线程的理解,我花了很长时间,搜索的大部份文章都不够通俗易懂。所以,这里力图用简单的例子,让你对多线程有个初步的认识。 单线程 在好些年前的MS-DOS时代,操作系统处理问题都是单任务的,我想做听音乐和看电影两件事儿,那么一定要先排一下顺序。 (好吧!我们不纠结在DOS时代是否有听音乐和看影的应用。^_^) from time import ctime,sleep def music(): for i in range(2): print "I was listening to music. %s" %ctime() sleep(1) def move(): for i in range(2): print "I was at the movies! %s" %ctime() sleep(5) if __name__ == '__main__': music() move() print "all over %s" %ctime() 我们先听了一首音乐,通过for循环来控制音乐的播放了两次,每首音乐播放需要1秒钟,sleep()来控制音乐播放的时长。接着我们又看了一场电影, 每一场电影需要5秒钟,因为太好看了,所以我也通过for循环看两遍。在整个休闲娱乐活动结束后,我通过 print "all over %s" %ctime() 看了一下当前时间,差不多该睡觉了。 运行结果: >>=========================== RESTART ================================ >>> I was listening to music. Thu Apr 17 10:47:08 2014 I was listening to music. Thu Apr 17 10:47:09 2014 I was at the movies! Thu Apr 17 10:47:10 2014 I was at the movies! Thu Apr 17 10:47:15 2014 all over Thu Apr 17 10:47:20 2014 其实,music()和move()更应该被看作是音乐和视频播放器,至于要播放什么歌曲和视频应该由我们使用时决定。所以,我们对上面代码做了改造:#coding=utf-8 import threading from time import ctime,sleep def music(func): for i in range(2): print "I was listening to %s. %s" %(func,ctime()) sleep(1) def move(func): for i in range(2): print "I was at the %s! %s" %(func,ctime()) sleep(5) if __name__ == '__main__': music(u'爱情买卖') move(u'阿凡达') print "all over %s" %ctime() 对music()和move()进行了传参处理。体验中国经典歌曲和欧美大片文化。 运行结果: >>> ======================== RESTART ================================ >>> I was listening to 爱情买卖. Thu Apr 17 11:48:59 2014 I was listening to 爱情买卖. Thu Apr 17 11:49:00 2014 I was at the 阿凡达! Thu Apr 17 11:49:01 2014 I was at the 阿凡达! Thu Apr 17 11:49:06 2014 all over Thu Apr 17 11:49:11 2014 多线程 科技在发展,时代在进步,我们的CPU也越来越快,CPU抱怨,P大点事儿占了我一定的时间,其实我同时干多个活都没问题的;于是,操作系统就进入了多任务时代。我们听着音乐吃着火锅的不在是梦想。 python提供了两个模块来实现多线程thread 和threading ,thread 有一些缺点,在threading 得到了弥补,为了不浪费你和时间,所以我们直接学习threading 就可以了。 继续对上面的例子进行改造,引入threadring来同时播放音乐和视频: #coding=utf-8 import threading from time import ctime,sleep def music(func): for i in range(2): print "I was listening to %s. %s" %(func,ctime()) sleep(1) def move(func): for i in range(2): print "I was at the %s! %s" %(func,ctime()) sleep(5) threads = [] t1 = threading.Thread(target=music,args=(u'爱情买卖',)) threads.append(t1) t2 = threading.Thread(target=move,args=(u'阿凡达',)) threads.append(t2) if __name__ == '__main__': for t in threads: t.setDaemon(True) t.start() print "all over %s" %ctime() import threading 首先导入threading 模块,这是使用多线程的前提。 threads = [] t1 = threading.Thread(target=music,args=(u'爱情买卖',)) threads.append(t1) 创建了threads数组,创建线程t1,使用threading.Thread()方法,在这个方法中调用music方法target=music,args方法对music进行传参。 把创建好的线程t1装到threads数组中。 接着以同样的方式创建线程t2,并把t2也装到threads数组。 for t in threads: t.setDaemon(True) t.start() 最后通过for循环遍历数组。(数组被装载了t1和t2两个线程) setDaemon() setDaemon(True)将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。子线程启动后,父线程也继续执行下去,当父线程执行完最后一条语句print "all over %s" %ctime()后,没有等待子线程,直接就退出了,同时子线程也一同结束。 start() 开始线程活动。 运行结果: >>> ========================= RESTART ================================ >>> I was listening to 爱情买卖. Thu Apr 17 12:51:45 2014 I was at the 阿凡达! Thu Apr 17 12:51:45 2014 all over Thu Apr 17 12:51:45 2014 从执行结果来看,子线程(muisc 、move )和主线程(print "all over %s" %ctime())都是同一时间启动,但由于主线程执行完结束,所以导致子线程也终止。 继续调整程序: ... if __name__ == '__main__': for t in threads: t.setDaemon(True) t.start() t.join() print "all over %s" %ctime() 我们只对上面的程序加了个join()方法,用于等待线程终止。join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞。 注意: join()方法的位置是在for循环外的,也就是说必须等待for循环里的两个进程都结束后,才去执行主进程。 运行结果: >>> ========================= RESTART ================================ >>> I was listening to 爱情买卖. Thu Apr 17 13:04:11 2014 I was at the 阿凡达! Thu Apr 17 13:04:11 2014 I was listening to 爱情买卖. Thu Apr 17 13:04:12 2014 I was at the 阿凡达! Thu Apr 17 13:04:16 2014 all over Thu Apr 17 13:04:21 2014 从执行结果可看到,music 和move 是同时启动的。 开始时间4分11秒,直到调用主进程为4分22秒,总耗时为10秒。从单线程时减少了2秒,我们可以把music的sleep()的时间调整为4秒。 ... def music(func): for i in range(2): print "I was listening to %s. %s" %(func,ctime()) sleep(4) ... 执行结果: >>> ====================== RESTART ================================ >>> I was listening to 爱情买卖. Thu Apr 17 13:11:27 2014I was at the 阿凡达! Thu Apr 17 13:11:27 2014 I was listening to 爱情买卖. Thu Apr 17 13:11:31 2014 I was at the 阿凡达! Thu Apr 17 13:11:32 2014 all over Thu Apr 17 13:11:37 2014 子线程启动11分27秒,主线程运行11分37秒。 虽然music每首歌曲从1秒延长到了4 ,但通多程线的方式运行脚本,总的时间没变化。 本文从感性上让你快速理解python多线程的使用,更详细的使用请参考其它文档或资料。 ========================================================== class threading.Thread()说明: class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}) This constructor should always be called with keyword arguments. Arguments are: group should be None; reserved for future extension when a ThreadGroup class is implemented. target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called. name is the thread name. By default, a unique name is constructed of the form “Thread-N” where N is a small decimal number. args is the argument tuple for the target invocation. Defaults to (). kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}. If the subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.__init__()) before doing anything else to the thread.
做开发或测试时常需要切换hosts ,如果hosts比较多,那么频繁的打开hosts文件对地址加注释(#),再把去掉注释是个繁琐的事情。 当然,SwitchHosts 已经可以帮我们方便的解决了这个繁琐的事情。 https://github.com/oldj/SwitchHosts 但笔者还是自己尝试用python写个小程序来实现切换。以需求为驱动来解决日常的问题是件非常有意思的事。 假如我们有一组hosts: 172.168.12.107 www.baidu.com 172.168.10.213 account.baidu.com 172.168.12.107 pan.baidu.com 172.168.12.107 passport.baidu.com 172.168.10.129 is.baidu.com 172.168.12.107 un.baidu.com 写代码之前想清楚几点。 1、hosts 文件一般放在我们的C:\WINDOWS\system32\drivers\etc\目录下,没有扩展名。我们可以通过记事本打开。python 的os模块可以用于打开本地文件。 2、我们要做的操作也很简单,加注释(加#号),去掉注释(去掉#号)。去掉注释时,当我打开浏览器访问www.baidu.com 时,其实访问的是本地的,172.168.12.107 主机。加上注释时,那么访问的就是真的百度服务器。 3、我们要做的操作是判断,每一行数据的第一个字符是否有#号,没有的话就加上。 打开python shell 练习加“#”号操作 >>> abc = '127.168.10.107 www.baidu.com' >>> a = abc[0] >>> if a != '#': nabc = '#'+abc print nabc #127.168.10.107 www.baidu.com 定义abc字符串,abc[0] 表示取字符串的第一个字符,判断是是否为#号,如果不是,就把#号加到abc字符串的前面。 添加注释的完整代码入下: #coding=utf-8 import os def add_jing(): input = open(r'C:\WINDOWS\system32\drivers\etc\HOSTS', 'r') lines = input.readlines() input.close() output = open(r'C:\WINDOWS\system32\drivers\etc\HOSTS', 'w') for line in lines: if not line: break jing = line[0] if jing != '#': print line nf = '#' + line output.write(nf) else: output.write(line) output.close() if __name__ == "__main__": add_jing() 程序先以读(r)的方式打开HOST文件,readlines() 方法逐行的读取内容。然后,close()关闭文件。 程序再以写(w)的方式打开HOST文件,对readlines() 获取的每一行数据判断是否有#号,没有的话加上。并通过write() 方法写入到HOST文件中。最后close()关闭文件。 打开python shell 练习“#”号操作: >>> abc = '#127.168.10.107 www.baidu.com' >>> a = abc[0] >>> if a == '#': nabc = abc.replace('#','') print nabc 127.168.10.107 www.baidu.com 同样取字符串的第一个字符判断,如果是#号,那么通过replace()方法 将#号替换成空(’’) 去掉注释的完整代码: def del_jing(): input = open(r'C:\WINDOWS\system32\drivers\etc\HOSTS', 'r') lines = input.readlines() input.close() output = open(r'C:\WINDOWS\system32\drivers\etc\HOSTS', 'w') for line in lines: if not line: break jing = line[0] if jing == '#': print line nf = line.replace('#','') output.write(nf) else: output.write(line) output.close() if __name__ == "__main__": del_jing() 通过运行add_jing() 和del_jing()两个函数的方式并不灵活。这里只是通过修改#的方式来切换hosts ,那么你也可以将hosts定义一个数组,直接写入到HOST文件。通过 写入不同的数组来达到切换不同hosts的目的。 #coding=utf-8 import os '''内网测试环境''' insides = ['172.168.12.107 www.baidu.com', '172.168.10.129 pan.baidu.com', '172.168.12.107 un.baidu.com', '172.168.12.107 passport.baidu.com'] '''外网测试环境''' outsides = ['172.16.12.223 www.baidu.com', '172.16.10.223 pan.baidu.com', '172.16.12.111 un.baidu.com', '172.16.12.223 passport.baidu.com'] def inside_test(): output = open(r'C:\pyse\HOSTS.txt', 'w') for insid in insides: print insid output.write(insid) output.write("\n") output.close() def outside_test(): output = open(r'C:\pyse\HOSTS.txt', 'w') for outsid in outsides: print outsid output.write(outsid) output.write("\n") output.close() if __name__ == "__main__": #inside_test() outside_test() 上面的方式会更加简单,把定义的host数组写到HOST文件中,注意:每写一个数组元素需要加一个回车换行---write("\n") 如果想继续增加切换host的便捷性,可以使用wxPython写一个host的配置界面出来,那么也就是我们的SwitchHosts 工具了。
注意:本标题的“自动化测试” 包括性能测试 与UI级的自动化测试 经常会被问到如何解决验证码的问题,在此记录一下我所知道的几种方式。 对于web应 用来说,大部分的系统在用户登录时都要求用户输入验证码,验证码的类型的很多,有字母数字的,有汉字的,甚至还要用户输入一条算术题的答案的,对于系统来 说使用验证码可以有效果的防止采用机器猜测方法对口令的刺探,在一定程度上增加了安全性。但对于测试人员来说,不管是进行性能测试还是自动化测试都是一个 棘手的问题。 下面来谈一下处理验证码的几种方法。 去掉验证码 这是最简单的方法,对于开发人员来说,只是把验证码的相关代码注释掉即可,如果是在测试环境,这样做可省去了测试人员不少麻烦,如果自动化脚本是要在正式环境跑,这样就给系统带来了一定的风险。 设置万能码 去掉验证码的主要是安全问题,为了应对在线系统的安全性威胁,可以在修改程序时不取消验证码,而是程序中留一个“后门”---设置一个“万能验证码”,只要用户输入这个“万能验证码”,程序就认为验证通过,否则按照原先的验证方式进行验证。 #coding=utf-8 import random #生成0到10之间的随机数 #d = random.uniform(0,10) #print d #生成一个1000到9999之间的随机整数 d = random.randint(1000,9999) print u"生成的随机数:%d " %d i = input(u"请输入随机数:") print i if i == d: print u"登录成功!!" elif i == 1111: print u"登录成功!!" else: print u"请重新输入验证码!" 运行结果: >>> ================================ RESTART ================================ >>> 生成的随机数:3764 请输入随机数:1111 登录成功!! >>> ================================ RESTART ================================ >>> 生成的随机数:3763 请输入随机数:3763 登录成功!! >>> ================================ RESTART ================================ >>> 生成的随机数:1928 请输入随机数:1354646 请重新输入验证码! random random用于生成随机数 randint() randint()方法用于生成随机整数,传递的两个参数分别是随机数的范围,randint(1000,9999)第二个参数要大于第一个参数。 我们要求用户输入随机数,并且对用户输入做判断,如果等于生成的随机数那么,登录成功,如果等于1111也算登录成功,否则失败。那么等于1111的判断就是一个万能码。 验证码识别技术 例如可以通过Python-tesseract 来识别图片验证码,Python-tesseract是光学字符识别Tesseract OCR引擎的Python封装类。能够读取任何常规的图片文件(JPG, GIF ,PNG , TIFF等)。不过,目前市面上的验证码形式繁多,目前任何一种验证码识别技术,识别率都不是100% 。 记录cookie (适用于UI自动化测试,且目前在大部应用的用户名密码不记录在cookie 或 进行加密处理。) 通过向浏览器中添加cookie 可以绕过登录的验证码,这是比较有意思的一种解决方案。我们可以在用户登录之前,通过add_cookie()方法将用户名密码写入浏览器cookie ,再次访问系统登录链接将自动登录。例如下面的方式: .... #访问xxxx网站 driver.get("http://www.xxxx.cn/") #将用户名密码写入浏览器cookie driver.add_cookie({'name':'Login_UserNumber', 'value':'username'}) driver.add_cookie({'name':'Login_Passwd', 'value':'password'}) #再次访问xxxx网站,将会自动登录 driver.get("http://www.xxxx.cn/") time.sleep(3) .... driver.quit() 使用cookie进行登录最大的难点是如何获得用户名密码的name ,如果找到不到name 的名字,就没办法向value 中输用户名、密码信息。 我建议是可以通过get_cookies()方法来获取登录的所有的cookie信息,从而进行找到用户名、密码的name 对象的名字;当然,最简单的方法还是询问前端开发人员。 总结: 最简单安全,行之有效的方式就是设置万能码,稍微和开发沟通一下就OK了。如果乐于“闷头苦干自力更生”的话也可研究验证码识别技术。
关于python读取xml文章很多,但大多文章都是贴一个xml文件,然后再贴个处理文件的代码。这样并不利于初学者的学习,希望这篇文章可以更通俗易懂的教如何使用python 来读取xml 文件。 什么是xml? xml即可扩展标记语言,它可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 abc.xml <?xml version="1.0" encoding="utf-8"?> <catalog> <maxid>4</maxid> <login username="pytest" passwd='123456'> <caption>Python</caption> <item id="4"> <caption>测试</caption> </item> </login> <item id="2"> <caption>Zope</caption> </item> </catalog> Ok ,从结构上,它很像我们常见的HTML超文本标记语言。但他们被设计的目的是不同的,超文本标记语言被设计用来显示数据,其焦点是数据的外观。它被设计用来传输和存储数据,其焦点是数据的内容。 那么它有如下特征: 首先,它是有标签对组成,<aa></aa> 标签可以有属性:<aa id=’123’></aa> 标签对可以嵌入数据:<aa>abc</aa> 标签可以嵌入子标签(具有层级关系): <aa> <bb></bb> </aa> 获得标签属性 那么,下面来介绍如何用python来读取这种类型的文件。 #coding=utf-8 import xml.dom.minidom #打开xml文档 dom = xml.dom.minidom.parse('abc.xml') #得到文档元素对象 root = dom.documentElement print root.nodeName print root.nodeValue print root.nodeType print root.ELEMENT_NODE mxl.dom.minidom 模块被用来处理xml文件,所以要先引入。 xml.dom.minidom.parse() 用于打开一个xml文件,并将这个文件对象dom变量。 documentElement 用于得到dom对象的文档元素,并把获得的对象给root 每一个结点都有它的nodeName,nodeValue,nodeType属性。 nodeName为结点名字。 nodeValue是结点的值,只对文本结点有效。 nodeType是结点的类型。catalog是ELEMENT_NODE类型 现在有以下几种: 'ATTRIBUTE_NODE''CDATA_SECTION_NODE''COMMENT_NODE''DOCUMENT_FRAGMENT_NODE''DOCUMENT_NODE''DOCUMENT_TYPE_NODE''ELEMENT_NODE''ENTITY_NODE''ENTITY_REFERENCE_NODE''NOTATION_NODE''PROCESSING_INSTRUCTION_NODE''TEXT_NODE' NodeTypes - 有名常数 http://www.w3school.com.cn/xmldom/dom_nodetype.asp 获得子标签 现在要获得catalog的子标签以的标签name <?xml version="1.0" encoding="utf-8"?> <catalog> <maxid>4</maxid> <login username="pytest" passwd='123456'> <caption>Python</caption> <item id="4"> <caption>测试</caption> </item> </login> <item id="2"> <caption>Zope</caption> </item> </catalog> 对于知道元素名字的子元素,可以使用getElementsByTagName方法获取: #coding=utf-8 import xml.dom.minidom #打开xml文档 dom = xml.dom.minidom.parse('abc.xml') #得到文档元素对象 root = dom.documentElement bb = root.getElementsByTagName('maxid') b= bb[0] print b.nodeName bb = root.getElementsByTagName('login') b= bb[0] print b.nodeName 如何区分相同标签名字的标签: <?xml version="1.0" encoding="utf-8"?> <catalog> <maxid>4</maxid> <login username="pytest" passwd='123456'> <caption>Python</caption> <item id="4"> <caption>测试</caption> </item> </login> <item id="2"> <caption>Zope</caption> </item> </catalog> <caption>和<item>标签不止一个如何区分? #coding=utf-8 import xml.dom.minidom #打开xml文档 dom = xml.dom.minidom.parse('abc.xml') #得到文档元素对象 root = dom.documentElement bb = root.getElementsByTagName('caption') b= bb[2] print b.nodeName bb = root.getElementsByTagName('item') b= bb[1] print b.nodeName root.getElementsByTagName('caption') 获得的是标签为caption 一组标签,b[0]表示一组标签中的第一个;b[2] ,表示这一组标签中的第三个。 获得标签属性值 <?xml version="1.0" encoding="utf-8"?> <catalog> <maxid>4</maxid> <login username="pytest" passwd='123456'> <caption>Python</caption> <item id="4"> <caption>测试</caption> </item> </login> <item id="2"> <caption>Zope</caption> </item> </catalog> <login>和<item>标签是有属性的,如何获得他们的属性? #coding=utf-8 import xml.dom.minidom #打开xml文档 dom = xml.dom.minidom.parse('abc.xml') #得到文档元素对象 root = dom.documentElement itemlist = root.getElementsByTagName('login') item = itemlist[0] un=item.getAttribute("username") print un pd=item.getAttribute("passwd") print pd ii = root.getElementsByTagName('item') i1 = ii[0] i=i1.getAttribute("id") print i i2 = ii[1] i=i2.getAttribute("id") print i getAttribute方法可以获得元素的属性所对应的值。 获得标签对之间的数据 <?xml version="1.0" encoding="utf-8"?> <catalog> <maxid>4</maxid> <login username="pytest" passwd='123456'> <caption>Python</caption> <item id="4"> <caption>测试</caption> </item> </login> <item id="2"> <caption>Zope</caption> </item> </catalog> <caption>标签对之间是有数据的,如何获得这些数据? 获得标签对之间的数据有多种方法, 方法一 #coding=utf-8 import xml.dom.minidom #打开xml文档 dom = xml.dom.minidom.parse('abc.xml') #得到文档元素对象 root = dom.documentElement cc=dom.getElementsByTagName('caption') c1=cc[0] print c1.firstChild.data c2=cc[1] print c2.firstChild.data c3=cc[2] print c3.firstChild.data firstChild 属性返回被选节点的第一个子节点,.data表示获取该节点人数据。 方法二 #coding=utf-8 from xml.etree import ElementTree as ET per=ET.parse('abc.xml') p=per.findall('./login/item') for oneper in p: for child in oneper.getchildren(): print child.tag,':',child.text p=per.findall('./item') for oneper in p: for child in oneper.getchildren(): print child.tag,':',child.text 方法二有点复杂,所引用模块也与前面的不一样,findall用于指定在哪一级标签下开始遍历。 getchildren方法按照文档顺序返回所有子标签。并输出标签名(child.tag)和标签的数据(child.text) 其实,方法二的作用不在于此,它核心功能是可以遍历某一级标签下的所有子标签。