2011年6月15日
#
首先需要申明的是,本人对javascript了解的不多,所以贻笑大方之处请见谅
我目前所负责的一个web项目其中前端部分所占比例较大,项目搭建于jsonp通讯(博客园中有一些介绍jsonp的相关帖子)基础之上,其特点很明显:
前端界面全部用js动态生成,且结构较复杂; 界面的生成需要服务器参与(异步回调中执行相关js函数以更新界面)
因此直接导致的结果便是开发后台服务器的同事需要跟前台同事纠结在一起,联调效率异常低下,而且每次版本升级之后都需要将以前的过程重新来过一次
痛定思痛,在对项目目前的处境做过评估和考量之后,我认为过程可以这样改进:
在前端代码中引入单元测试机制,将所有代码纳入自动化单元测试框架之中
如何重构
随之而来的问题很明显:
代码如何合理的组织才能方便进行单元测试?
界面如何单元测试?
首先,代码如何组织其实很简单,MVC结构很好的解决了这个问题,与界面无关的代码提取到M和C中即可,M和C是可以方便进行单元测试的
说到界面如何测试,可能有的人会想到Selenium这类界面流程录制工具,这类工具不能说不牛叉,但对于单元测试来说有个最大的缺憾:无法自动化,自动集成部署完全无法做
对于界面的自动化测试,我是这样考虑的:
所有web界面的更新,无非是在指定的区域插入或删除指定的html结构片段
基于这点考虑,我定义了一个衔接于MC和V的UI解析层次,并制定了几个基本的规则,例如要在一个div中插入新的innerHtml,可以这样定义:
{‘action’: ‘insert’, ‘target’: ‘div_id’, ‘src’: ‘…’}
在M和C做完代码处理之后,如果需要更新界面,将动态生成的html片段(可能以模板技术动态生成)以这种格式返回即可
那么,有了这个UI解析层的加入,针对界面的单元测试便可轻松搞定了,对于具体的需求,只需要比较对应的界面定义返回值是否满足期望即可,完全无需真正的web界面参与
如何单元测试
在js的单元测试框架的选型上,起先我选择了QUnit(http://docs.jquery.com/QUnit ),其用法很简单,界面很赞,而且也基本上没有任何侵入性,但有2点比较伤不起:
无法自动化,测试结果只能在web页面中查看
无法输出相应报表 //js本身安全限制
因此我考虑是否有其他语言能解释运行js,并且自身有成熟的单元测试框架,当然这种要求是完全可以满足的:py spidermonkey(http://code.google.com/p/python-spidermonkey/ ),这是一个针对python的开源js解释运行库,和当前最新版的ff4内核完全一致,单元测试有了python的参与,自然如虎添翼,以下代码便是一个示例:
utbase.py
import unittest
import spidermonkey
from os import path
import json
__author__ = ‘Michael’
class ClientUTBase(unittest.TestCase):
def initialize(self):
self.__cx = spidermonkey.Runtime().new_context()
self.__cx.add_global('loadfile', self.__loadfile)
def __loadfile(self, fn):
with open(fn, 'r') as f:
return f.read()
def readyJsFile(self, jsfilepath):
'''
jsfilepath: javascript filepath
'''
if not jsfilepath:
raise IOError('jsfilepath is invalid')
if not path.isfile(jsfilepath):
raise IOError('jsfilepath %s not existed' % jsfilepath)
param = 'eval(loadfile("%s"))' % jsfilepath
self.__cx.execute(param)
def runJs(self, jsFuncName, *params):
encode = "%s(%s)" % (jsFuncName, ', '.join(map(lambda p: json.dumps(p), params)))
return self.__cx.execute(encode)
demo_test.py
#coding:utf-8
from utbase import ClientUTBase
import unittest
__author__ = 'Michael'
class XXXTestCase(ClientUTBase):
'''
XXX: 对应业务的名称
'''
def setUp(self):
self.initialize() #must initialize ClientUTBase object at first
jsfn = '1.js'
self.readyJsFile(jsfn) #load js file
def tearDown(self):
pass
def test_getnull(self):
jsFuncName = 'getNULL'
rt = self.runJs(jsFuncName) #invoke js function, get return value
print rt #rt == None here
self.assertTrue(not rt)
def test_getName(self):
jsFuncName = 'getName'
rt = self.runJs(jsFuncName, 'hello')
self.assertNotEqual(rt, 'world')
def test_nofuncDef(self):
jsFuncName = 'getXXX' #no such function in js file
self.assertTrue(self.runJs(jsFuncName))
def test_getComplexObject(self):
obj = {'name' : 'mic'}
jsFuncName = 'getComplexObject'
rt = self.runJs(jsFuncName, obj)
self.assertEqual(rt, obj) #oh yeah
if __name__ == "__main__":
suite = unittest.TestLoader().loadTestsFromTestCase(XXXTestCase)
unittest.TextTestRunner(verbosity=2).run(suite)
1.js
function err() {
throw new Error('hello world');
}
function getNULL() {
return null;
}
function getName(myname) {
return myname;
}
function getComplexObject(obj) { //obj: {'name' : 'mic'}
return obj;
}
但是,py spidermonkey有个限制:目前还没有windows版本,只能在linux和Mac执行,不过这也不是神马大问题
2011年3月26日
#
事件起因
自从去年11月份跳槽之后,就告别了熟悉的c#,涉险python开发,顺便开始熟悉管理之道
最近一直忙于一个新产品的beta版上线公测,该产品涉及到好几个子系统,为了节约空间,这些子系统全部部署在一台64位Linux机器上,通过不同端口访问,惊险之旅自此开始…
项目情况大致如下
A子系统: nginx管理静态文件,动态请求基于jsonp方式访问不同域名下的网关应用
B子系统: nginx管理静态文件,动态请求通过nginx反向代理映射到fastcgi+webpy模块上,认证方式使用webpy内置的session机制
测试环境包括平时的开发测试和部署之后的内部测试,不久之前测试人员经常跟我反映:
部署在内网中的B子系统在用浏览器登录几分钟之后就出现频繁的404回应,根本无法使用 ,但在强制清除cookie之后又可以正常使用,不久之后又恢复不能访问现象…
分析
起初我很是奇怪,按理说只有当静态文件不存在时nginx才会返回404错误,而现在访问的是配置在nginx.conf中的一个动态路径,该动态路径请求通过fastcgi最终会映射到某个python class的GET或POST方法中,那为什么nginx会返回404呢?
查阅相关文档之后发现
fastcgi在遇到webpy或其他后端http模块处理极慢的情况下,也就是说超过nginx允许的应答时间,nginx就会对此动态路径请求做出404的应答
针对此情况,我开始着手准备一系列测试和实验
实验
实验一:剥离fastcgi,单独使用webpy,进行压力测试
这个实验很简单,只需要注释掉一行代码,便可以以纯webpy方式访问,基于webpy的python应用main入口一般都是这样:
if __name__ == "__main__":
web.wsgi.runwsgi = lambda func, addr=None: web.wsgi.runfcgi(func,addr)
app.add_processor(web.loadhook(session_hook))
app.run()
只需要注释掉第二行即可,之后在终端下运行python <filename>.py,webpy会默认监听本地的8080端口,之后无论是通过浏览器还是其他方式访问相应地址即可
同时,还需要准备一份发起http请求的代码,python用来干这活最简单不过了
import httplib
server = "192.168.2.2:8080"
url = "/user/login?username=xxx@domain.com&password=111&rnd=33"
class perftest:
def __init__(self):
pass
def run(self):
conn = httplib.HTTPConnection(server)
for i in range(100):
try:
conn.request("GET", url)
rsp = conn.getresponse()
if 200 == rsp.status:
print "headers", rsp.getheaders()
content = rsp.read()
print "content", content
except Exception, ex:
print "ERR:", ex
conn.close()
跑了几次压力之后,webpy扛不住了…开始大面积抛异常,大致意思是python自带的logging模块出错,因为有频繁的文件打开关闭操作,某个操作导致文件句柄被非法访问…
在查看相关的log封装类之后发现,python的日志模块设计实在是有点不够内聚,也可以说不够彻底吧,不由得想起了之前用过的log4net,真是简洁啊
出错的原因在项目中开发同事封装的log模块多次执行了addHandler和removeHandler操作,据说在之前的开发过程中还出现过一行日志打印多遍的情况,而且随着程序的运行,相同的日志会越来越多…
顺手将该封装修改为单例类之后,故障解除
处理完之后接着跑压力,webpy还是会偶尔打印异常信息,不过已经变成了session访问异常,还好的是出现面积很小,于是开始增加压力测试的并发量,结果表明稳定度还是可以的,性能也在可以接受的范围之内
实验二:保持最终部署环境不变(nginx+fastcgi+webpy),继续压力测试
在上次测试解决完日志的问题之后,我开始以真实环境做相关的测试,要注意的是使用nginx之后,需要访问nginx.conf中配置绑定的相关端口,不再是webpy默认的8080端口
继续做上述的压力测试,性能也在可以接受的范围之内
实验三:模拟用户环境
根据测试MM所描述的,浏览器在B子系统出错过程中扮演了很关键的角色,于是我分别使用几个浏览器和纯粹的python代码分别访问相同的地址
几种组合的测试发现:
浏览器在只访问B子系统的情况下,一直表现正常
一旦浏览器访问过A子系统之后,再次访问B子系统即出现测试MM描述的404现象
python代码一直访问正常
那么,我猜想问题原因应该在浏览器的某种特殊行为导致了该现象的出现,仔细想想,浏览器和python脚本的唯一区分在于浏览器有保存和发送cookie的行为,而python脚本没有,那么,可以抓包看看
抓包发现,在只访问B子系统的情况下,http头的发送Cookie只有由webpy设置的set-cookie信息,即web_session_id字段,访问过A子系统之后,出现了其他的字段,即截图中的
此时我才想起来A,B子系统部署在同一台机器上,而且代码都在同一个根目录下,于是A子系统的cookie信息便被浏览器发送到B子系统的webpy环境中解析
为了进一步证实我的想法,我在python脚本中直接使用socket方式,模拟了浏览器的http头信息,直接请求webpy的8080端口,注意,此时需要打开webpy的debug开关
果然,webpy马上报错了
打开python库的相关代码,发现:
D:\Python27\Lib\Cookie.py
line 247
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~"
合法的cookie key唯独就没有@符号…
总结
至此,bug的根源算是找到了,但我的心情却很是复杂,以往的代码review过程发现开发同事的开发习惯不是很理想,代码编写非常随意,结构比较混乱,由此带来的问题便是代码质量不高,bug频现,该怎么解决这个病瘤一直是我心中的痛…
同时,我们也发现,在服务器代码开发中一定要避免出现异常捕获,由此带来的性能问题和其他诡异的现象非常令人头痛,还有,谙熟相关标准显然也是非常重要的
2010年9月27日
#
SPlayer
1 class SPlayer 2 { 3 private static SoundPriorities _lastPriority = SoundPriorities.Low; 4 5 public static bool Play( string wavPath, SoundPriorities priority) 6 { 7 if ( string .IsNullOrEmpty(wavPath)) return false ; 8 if ( ! File.Exists(wavPath)) return false ; 9 if ( ! wavPath.EndsWith( " .wav " , StringComparison.OrdinalIgnoreCase)) return false ; 10 11 if (priority >= _lastPriority) 12 { 13 Stop(); 14 _lastPriority = priority; 15 return NativeMethods.PlaySoundA(wavPath, new IntPtr(), ( int )(PlaySoundFlags.SND_ASYNC | PlaySoundFlags.SND_LOOP)); 16 } 17 18 return false ; 19 } 20 21 public static void Stop() 22 { 23 if (NativeMethods.PlaySoundA( null , new IntPtr(), 0 )) 24 _lastPriority = SoundPriorities.Low; 25 } 26 } 27 28 [Flags] 29 enum PlaySoundFlags : int 30 { 31 SND_SYNC = 0x0000 , 32 SND_ASYNC = 0x0001 , 33 SND_NODEFAULT = 0x0002 , 34 SND_LOOP = 0x0008 , 35 SND_NOSTOP = 0x0010 , 36 SND_NOWAIT = 0x00002000 , 37 SND_FILENAME = 0x00020000 , 38 SND_RESOURCE = 0x00040004 39 } 40 41 enum SoundPriorities : int 42 { 43 Low = 0 , 44 Normal = 1 , 45 High = 2 46 } 47 48 [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] 49 public struct HINSTANCE__ 50 { 51 52 /// int 53 public int unused; 54 } 55 56 public partial class NativeMethods 57 { 58 59 /// Return Type: BOOL->int 60 /// pszSound: LPCSTR->CHAR* 61 /// hmod: HMODULE->HINSTANCE->HINSTANCE__* 62 /// fdwSound: DWORD->unsigned int 63 [System.Runtime.InteropServices.DllImportAttribute( " winmm.dll " , EntryPoint = " PlaySoundA " )] 64 [ return : System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)] 65 public static extern bool PlaySoundA([System.Runtime.InteropServices.InAttribute()] [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)] string pszSound, System.IntPtr hmod, uint fdwSound); 66 67 }
雕虫小技啊,不足挂齿 ,唯一要注意的是:
根据msdn文档所描述,该api(winmm:PlaySound)播放任意新声音文件会自动停止之前已播放的内容,所以...
当然这个类还可以写的更复杂,这个就不说了
2010年9月20日
#
写过Winform程序的同学或许都遇到过这样的异常:
Title
在某个线程上创建的控件不能成为在另一个线程上创建的控件的父级
遇到这个问题之后,第一反应当然是g一把,解决方案大多数都是类似这样的代码:
1 if (XXX.InvokeRequired) 2 { 3 // ... 4 } 5 else 6 { 7 // ... 8 }
这段代码的原理是将
1 ControlCollection.Add(Control);
转移到主线程中执行,貌似能解决哦?但是很多情况下并不能解决根本问题,异常照样会抛出来,具体原因我也不得而知,但是至少我们从可以绕道而行之...
上文提到在主线程中添加子控件的原理,那么我们可以从此下手,很显然,windows消息的触发一定是在主线程中执行的,在尝试之后我总结了2种情况:
1.简单子控件
如果只需要添加个按钮啥的,可以直接利用父控件的Paint消息,因为很简单,要注意的是Paint消息会不定时触发,需避免重复添加子控件
2.复杂子控件
如果子控件中包含各种COM控件,而且还包含耗时的业务调用,这时就得小心了,如果继续在Paint消息中处理,很可能会导致界面刷新不及时,而且这种方式确实有点山寨...
那具体要怎样做呢?
调用Windows API之PostMessage(Intptr, int, int, int)向某窗口发送消息,当然SendMessage亦可 一般情况下,该窗口对象可以重写WndProc方法分发该消息,但是还有更优雅的方式:继承IMessageFilter接口,拦截所有的Windows消息进行分发 处理类对象接收到对应的windows消息后创建子控件对象后添加
OK,总结吧
优点:很显然,无需担心COM控件的创建以及方法调用,实乃居家旅行杀人越货之良品 缺点:需要创建诸多的类成员变量(不优雅),以及需要仔细控制方法调用顺序,更可能需要对已有的代码结构进行仔细的重构,很好很山寨...
2010年3月25日
#
实际上有点标题党了哈,但实际上这个bug从.net 2.0一直到现在都是存在的,而且m$也从来没有真正解决过,先看段测试代码
namespace TestMDA
{
class Program
{
static void Main(string[] args)
{
People p = new People()
{
Age = 1,
Name = "Michael"
};
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
Console.WriteLine(e.Name);
return null;
};
using (FileStream fs = new FileStream("test.txt", FileMode.Create))
{
XmlSerializer xs = new XmlSerializer(typeof(People)); //throw MDA binding fail here
xs.Serialize(fs, p);
}
Console.ReadLine();
}
}
public class People
{
public int Age { get; set; }
public string Name { get; set; }
}
}
上面这段代码无论通过VS05抑或VS08编译之后,运行都会输出
TestMDA.XmlSerializers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
TestMDA.XmlSerializers
但是实际上序列化依然可以照常进行……
更诡异的事实是,如果修改相对应的namespace,例如 namespace ABC {…},就会输出
ABC.XmlSerializers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ABC.XmlSerializers
----------------------- 分割线------------------------
为什么提到这个话题呢?我们现在进行着的一个项目中某些代码使用了XML序列化方式,随着程序的运行就会看到类似上面这种日志……苦思不得结果,找遍了引用程序集也没看到对应的dll或exe,想想既然无大碍就暂时没去深究,不过这几天刚好项目要做一个部署分发,在测试时刚好也发现类似问题,遂google之,发现2007年帖子一枚
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=304095&wa=wsignin1.0
m$的说法是这个是by design的,不过直到现在.net 3.5也有这毛病,我也就不奢望啥了…
ps.当时应该是beta版本的VS,遇到这种情况会抛出异常,还好现在不会抛异常,但是也很郁闷 :)
2009年9月13日
#
摘要: 前情提要: http://www.cnblogs.com/lovesanni/archive/2009/09/08/1562844.html前面说到模仿Control类自身的BeginUpdateInternal和EndUpdateInternal方法,封装各个控件都适用的基础类,先看代码吧Code highlighting produced by Actipro CodeHighlighter ...
阅读全文
2009年9月8日
#
摘要: 适用场景:需要在某容器控件中动态装载多个子控件,而且该容器控件可能需要改变WindowFormState,即从Normal转变为Maxmized,或者是其他状态转换啦, what ever :)如果没有应用任何特殊处理,你就会发现,当容器控件状态转换时,其上的子控件在经过一阵狂闪之后(可能背景控件颜色和自身相互交替出现),最终恢复至平静; 这种情形当然无论是程序员自己和客户都不愿意看到的ok,废话...
阅读全文
2009年5月21日
#
摘要: 我于2009年4月26日在贤贤网(贤智创富资产管理顾问(北京)有限公司)订下机票(2009年5月10日 厦门 - 北京),金额总计¥758.0元,包括机票+保险+10元快递费订票成功后,因为其中涉及机票快递流程不清楚曾交涉过该网站客服,当时的答复是机票不负责送至客户,只是随其产生的保险单快递至客户,后来我的确通过该航班航空公司查询到机票已订成功,就暂时未曾催促贤贤网快递保险单在2009年4月26日...
阅读全文
2008年8月15日
#
摘要: 怎样解决FileSystemWatcher多次触发问题?
阅读全文
2008年7月22日
#