无GUI的server上通过虚拟显示来运行RobotFramework+Selenium2 (webdriver)

RobotFramework是强大的开源自动化测试框架,与selenium2(webdriver)结合,可以自动化对网页应用进行测试,甚至A-TDD。

selenium需要打开浏览器,而浏览器需要图形化界面。如果你在liunx服务器上直接运行firefox,你会得到:

1
2
# firefox
Error: no display specified

如何在这种限制下进行Robot测试呢?

Xvfb是流行的虚拟现实库,可以使很多需要图形界面的程序虚拟运行。

pyvirtualdisplay是该库的python封装,进一步我们可以将其封装在Keywords中。那么你的Robotcase可能看起来是这样:

~ ~ ~
Run Keyword If ‘${IsVirtualDisplay}’==’NO’ Open Virtual Display
Open Browser http://someurl firefox
Some Operations…
Close All Browsers
Run Keyword If ‘${IsVirtualDisplay}’==’NO’ Close Virtual Display

在本地具有GUI的机器进行Test case调试时,可以关闭该开关,而在CI服务器配置中,可以设置开关来打开虚拟显示。

1
pybot -v IsVirtualDisplay:YES example.tsv

另外,Selenium2Library是在GitHub上的开源项目,很好将selenium2(webdriver)封装成Robot Keyword,但是往往这些还不够用。
肯定需要实现产品特性专用的Keyword Library。
如果单独写一个库,会无法享受到Selenium2Library封装好的,特别是对浏览器句柄封装好的keywords。
因此,可以考虑继承Selenium2Library,注意,该库使用了Decorator来将分布在不同类中的public方式暴露并组装成整体。

好用的Git config 缩写配置

初次安装运行Git,建议在命令行中运行以下命令,配置缩写和颜色,支持中文文件名显示和提交等。
可以有效提高效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git config --global --add user.email mebusw@163.com
git config --global --add user.name JackyShen
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.last 'log -1 HEAD'
git config --global color.diff auto
git config --global color.status auto
git config --global color.branch auto
git config --global alias.co checkout
git config --global core.quotepath false
git config --global merge.tool kdiff3
git config --global meregtool.kdiff3.path /usr/bin/kdiff3
git config --global alias.visual !gitk

建立忽略文件 ~/.gitignore_global

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
###################
*.com
*.class
*.dll
*.exe
*.o
*.so
*.pyc

# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip

# Logs and databases #
######################
*.log
*.sql
*.sqlite

# OS generated files #
######################
.DS_Store*
ehthumbs.db
Icon?
Thumbs.db
.svn
*.bak

Scrum 2011更新

  • 进行增量产出的一组开发人员组成一个开发团队(development team)。如果大家都在单打独斗的开发,那不叫团队.
  • 在冲刺计划会(sprint planning)上,团队不再承诺(commit)所有工作,而是建立一个相信能够完成任务的预期(forcast),但是这个预期在整个sprint中,随着对任务的深入了解而变化。
  • 不再必须用燃尽图(burndown chart)来监控进度。Scrum只需要每天对剩余工作进行估计,且在整个sprint中,持续维护任务完成的趋势即可。
  • 版本计划会(release planning)对使用scrum来说很重要,但不是scrum本身的要求。
  • 冲刺订单(sprint backlog)相当于从产品订单(product backlog)中选择出来的任务,加上交付任务的计划。冲刺订单任务(sprint backlog item)的概念不再需要,尽管这种技术可以做出伟大的计划。一个自我组织的开发团队总是有其自己的计划。
  • 产品订单是有序排列的,而不再要求是按优先级排列的。这样允许产品负责人(product owner)在特定环境下灵活的优化产品价值。

原文:

http://www.scrum.org/storage/Scrum%20Update%202011.pdf

Java NIO Socket实现C/S架构

一般的Server端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package thread.socket;  
import java.io.*;
import java.net.*;
import java.util.*;
public class Server extends ServerSocket {
private static final int SERVER_PORT = 10000;
private List<WorkerThread> workers;

public Server() throws IOException {
super(SERVER_PORT);
workers = new LinkedList<WorkerThread>();

try {
System.out.println("server is listening...");
while (true) {
Socket socket = accept();
workers.add(new WorkerThread(socket));
System.out.println(String.format("new worker created, total %d", getWorkerCount()));
}
} catch (IOException e) {
} finally {
close();
}
}
public synchronized int getWorkerCount() {
return workers.size();
}
public static void main(String[] args) throws IOException {
new Server();
}
}
// --- WorkerThread
class WorkerThread extends Thread {
private Socket client;
private BufferedReader in;
private PrintWriter out;
public WorkerThread(Socket s) throws IOException {
System.out.println(String.format("create a new thread. %s", s));
client = s;
in = new BufferedReader(new InputStreamReader(client
.getInputStream(), "GB2312"));
out = new PrintWriter(client.getOutputStream(), true);
out.println("--- Welcome ---" + client.getRemoteSocketAddress());
start();
}
public void run() {
try {
String line = in.readLine();
while (!line.equals("bye")) {
System.out.println("client " + client.getRemoteSocketAddress() + " says: " + line);
String msg = createMessage(line);
out.println(msg);
line = in.readLine();
}
out.println("bye");
System.out.println("client " + client.getRemoteSocketAddress() + " quit");
client.close();
} catch (IOException e) {
}
}
private String createMessage(String line) {
// ;
return "response to " + line;
}
}

使用NIO的Server端 (从1.5开始,Java对InputStream/OutputStream 进行了重新改写,用的就是NIO,因此,就算你不显示声明要用NIO,只要你的类继承了InputStream/OutputStream就已经在用NIO了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import java.io.BufferedWriter;  
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
public class SelectorServer
{
private static int DEFAULT_SERVERPORT = 6018;//默认端口
private static int DEFAULT_BUFFERSIZE = 1024;//默认缓冲区大小为1024字节
private static String DEFAULT_CHARSET = "GB2312";//默认码集
private static String DEFAULT_FILENAME = "bigfile.dat";
private ServerSocketChannel channel;
private LinkedList<SocketChannel> clients;
private Selector selector;//选择器
private ByteBuffer buffer;//字节缓冲区
private int port;
private Charset charset;//字符集
private CharsetDecoder decoder;//解码器


public SelectorServer(int port) throws IOException
{
this.port = port;
this.clients = new LinkedList<SocketChannel>();
this.channel = null;
this.selector = Selector.open();//打开选择器
this.buffer = ByteBuffer.allocate(DEFAULT_BUFFERSIZE);
this.charset = Charset.forName(DEFAULT_CHARSET);
this.decoder = this.charset.newDecoder();

}

private class HandleClient
{
private String strGreeting = "welcome to VistaQQ";
public HandleClient() throws IOException
{
}
public String readBlock()
{//读块数据
return this.strGreeting;
}
public void close()
{

}
}
protected void handleKey(SelectionKey key) throws IOException
{//处理事件
if (key.isAcceptable())
{ // 接收请求
ServerSocketChannel server = (ServerSocketChannel) key.channel();//取出对应的服务器通道
SocketChannel channel = server.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);//客户socket通道注册读操作
}
else if (key.isReadable())
{ // 读信息
SocketChannel channel = (SocketChannel) key.channel();
int count = channel.read(this.buffer);
if (count > 0)
{
this.buffer.flip();
CharBuffer charBuffer = decoder.decode(this.buffer);
System.out.println("Client >>" + charBuffer.toString());
SelectionKey wKey = channel.register(selector,
SelectionKey.OP_WRITE);//为客户sockt通道注册写操作
wKey.attach(new HandleClient());
}
else
{//客户已经断开
channel.close();
}
this.buffer.clear();//清空缓冲区
}
else if (key.isWritable())
{ // 写事件
SocketChannel channel = (SocketChannel) key.channel();
HandleClient handle = (HandleClient) key.attachment();//取出处理者
ByteBuffer block = ByteBuffer.wrap(handle.readBlock().getBytes());
channel.write(block);
// channel.socket().getInputStream().(block);
// PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
// channel.socket().getOutputStream())), true);
// out.write(block.toString());
}
}
public void listen() throws IOException
{ //服务器开始监听端口,提供服务
ServerSocket socket;
channel = ServerSocketChannel.open(); // 打开通道
socket = channel.socket(); //得到与通到相关的socket对象
socket.bind(new InetSocketAddress(port)); //将scoket榜定在制定的端口上
//配置通到使用非阻塞模式,在非阻塞模式下,可以编写多道程序同时避免使用复杂的多线程
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);
try
{
while(true)
{// 与通常的程序不同,这里使用channel.accpet()接受客户端连接请求,而不是在socket对象上调用accept(),这里在调用accept()方法时如果通道配置为非阻塞模式,那么accept()方法立即返回null,并不阻塞
this.selector.select();
Iterator iter = this.selector.selectedKeys().iterator();
while(iter.hasNext())
{
SelectionKey key = (SelectionKey)iter.next();
iter.remove();
this.handleKey(key);

}
}
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
public static void main(String[] args) throws IOException
{
System.out.println("服务器启动");
SelectorServer server = new SelectorServer(SelectorServer.DEFAULT_SERVERPORT);
server.listen(); //服务器开始监听端口,提供服务
}
}

使用NIO的Client端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package thread.socket;  
import java.io.*;
import java.net.*;
public class Client {
Socket socket;
BufferedReader in;
PrintWriter out;
public Client() {
try {
socket = new Socket("127.0.0.1", 10000);
in = new BufferedReader(new InputStreamReader(socket
.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader line = new BufferedReader(new InputStreamReader(
System.in));
String cmd = "";
while (!cmd.equals("bye")) {
System.out.println("server says: " + in.readLine());
out.println(cmd = line.readLine());
}
System.out.println("socket " + socket + " stop");
line.close();
out.close();
in.close();
socket.close();
} catch (IOException e) {
}
}
public static void main(String[] args) {
new Client();
}
}

Do you like my piece of paper?

What I see every day is people creating pieces of paper to take into a room for people to look at and decide if they like that piece of paper, and if they don’t totally like it they say what they would prefer to see on that piece of paper.

This repeats until everybody is happy with a piece of paper.

capture3

Read More

TDD中变形动作的优先顺序 - Transformation Priority Premise

在TDD循环中,重构(Refactoring)是不改变行为而改变内部结构的动作,保持测试常绿。而变形(Transformation)是改变内部实现来使测试由红变绿。这些变形的变化使代码形式从特殊specific到一般generic。

Uncle Bob的一篇文章。http://blog.8thlight.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html
中用了bowling game kata 和 Prime Factor kata来解释。

Read More

“设计”工作应该放在迭代中吗?

在教练敏捷团队的过程中,一些团队反映“某些用户故事不好拆分,无法在一个迭代内完成”。对于这些故事,一些团队的做法是先指定资深人员作为“设计师”,他从“需求分析师”或业务方拿到需求后,花上1个迭代的时间进行“设计”,然后由他将编码任务进一步分配给“开发人员”,并且负责验收代码。这些故事往往就会持续1~2个迭代才能完成,甚至还来不及测试。

Read More

所有代码都需要单元测试覆盖吗?

test-coverage

单元测试(unit testing)已经越来越得到广大开发者的认可。作为低成本、速度快、稳定度高的自动化测试手段,单元测试可以在类和函数级别对代码进行质量守护,有助于避免尴尬、耗时的错误。当然,相比功能测试(Functional testing)和端到端测试(end-to-end testing),单元测试能够寄予的产品级别的信心要略低一些,因而各个粒度的测试应该是相辅相成的,互为补充。

常常听到一些组织要求开发团队提高单元测试覆盖率,换来的却是怨声载道,或者是一堆应付差事的垃圾测试(没有断言的测试,都见过吧)。尽管,低测试覆盖率意味着对质量的信心不足,但是,单元测试覆盖率真的要达到100%才好吗?

Read More