Ma client server简明教程

翻译:ShiningRay @ NirvanaStudio
原文地址:http://minnow.cc.gatech.edu/squeak/2978

{译者:Ma client server是针对Smalltalk的一个网络应用框架,由于十分好用,介绍到这里来}

简介

Ma client server 可以帮助你非常方便地书写客户端/服务器端(Client/Server,以下简称C/S)程序。事实上,它容易到了服务器段程序只要用一行代码:

MaServerSocket new listenOn: 12345 answer: [ :requestByteArray | 'Hello world!' asByteArray ]

这个框架实际上是基于一个稳定可靠的、多线程的Socket模型,是由John McIntosh分享给我的。

在C/S模型中,一个客户端向服务器段发送一个请求,然后服务器处理请求并返回一个应答。

这个框架提供了相互配合的两层结构。你选择其中你想使用的一层然后写两个程序。一个客户端程序会给远程服务器发送请求,服务器上则运行着另一个程序处理请求。

如果你选择较低的一层,“套接字层”(Socket Layer),你发送的是ByteArray(字节数组,一个基本类)的请求,接收的是ByteArray的应答。在较高一层,“对象请求层”(Object Rquest Layer),你可以发送任意的请求对象,获取任意的对象作为应答。较高层上使用了“Ma对象序列化”(Ma Object Serialization)框架来将对象转换成ByteArray,然后让较低的一层为你把这个ByteArray发送出去。

使用套接字层

建立服务器

如果你只是要发送和接收ByteArray的数据,你可以随便写你的服务器程序。但记住,不管在哪里,你要接收ByteArray作为输入,你就必须应答一个ByteArray

然后,要启动服务器:

myServerSocket _ MaServerSocket new
listenOn: 12345
answer:
[ :requestByteArray |
... 将 requestByteArray 传给你的服务器端程序 ... ]

MaServerSocket 利用了 SharedQueue和后台进程来确保处理过程不会因在网络上发送和接收ByteArray数据而阻塞,现在发送和接收完全在后台进行。“answer”块一次对一个客户端请求进行执行。

当你完成之后,一定要关闭套接字这样资源才能正确被释放:

myServerSocket shutdown

在客户端获得应答

下面客户端程序建立了一个MaClientSocket对象,要将IP地址作为ByteArray中的数字指明:

mySocket _ MaClientSocket hostAddress: #(192 168 1 1) asByteArray port: 12345.

建立了套接字之后还没有进行任何连接。只有在你发送请求的时候才会发生:

responseByteArray _ mySocket sendData: myRequestByteArray

同时具有一个sendData:startingAt:count:waitForReplyIn:方法可以达到最大的效率。如果你提供作为返回的ByteArray 不够大,你就会得到一个更大的。

可选的套接字事件

MaServerSocket 要用到一个 “server”, 默认是一个MaServer对象不过它什么也不做。但如果你给MaServer派生子类并且重写其中一些方法,这些方法会在如下时候调用套接字加入队列、一个ByteArray请求完全抵达,或者是发送一个响应等等。详细信息见MaServer

使用对象请求层

如果你选择使用对象请求层,你发送“请求”对象并获得某种应答对象。这样你可以专心写客户端和服务器程序,而不用关系如何解析ByteArray了。

建立服务器

这里当你准备开始写你的服务器段程序时,不是建立一个MaServerSocket对象,而是建立一个MaTcpRequestServer对象:

myServer _ MaTcpRequestServer protocol:
{ MyRequestClass1.
MyRequestClass2.
MyResponseClass1.
MyResponseClass2.
OrderedCollection.
Dictionary. "etc." }.

这是什么?是的,你必须告诉她你要发送和接收的所有类,来记录你请求对象的形式。例如,如果你有一个 SubmitArticleRequest类,里面有一个Article对象,你不仅要列出SubmitAritcleRequest类,还要给出Article和所有其他Article引用的类和这些类他们引用的类,如此继续。然而,你不需要给出String, ByteArray, Set, Symbol, SmallInteger, Float, True, False或者UndefinedObject等类。这些类已经自动成为协议的一部分了。

也可以指明一个MaServerConsole来接收事件。派生MaServerConole并改写他的一个通知方法,象下面这样,他就会被自动调用。

myServer _ console: myConsole

当你准备启动服务器时,你要使用 processOn:do:

myServer
processOn: 12345 ;
using:
[ :requestObject |
... 将 requestObject 传递给服务器程序,可以应答响应 ... ]

当完成之后,你需要关闭并释放系统资源:

myServer shutdown

建立客户端

你的客户端程序建立一个 MaTcpRequestServerLink 并指向服务器所监听的地址 hostAddress 和端口 port

myLinkToServer _ MaTcpRequestServerLink hostAddress: #(192 168 1 1) asByteArray port: 12345

在你要做任何事之前,你必须先建立连接。这确保你在客户端的镜像中包含了所有必须的协议 (如果没有就会抛出异常)。

myLinkToServer connect

如果要在网络状况和你的耐性做出平衡,你可能想改写默认的超时期限为30秒:

myLinkToServer timeoutSeconds: 30

在客户端获得应答

现在你已经连上了,你的客户端程序可以如下从服务器端获得应答:

myLinkToServer submit: someRequestObject

记住, someRequestObject 必须是协议的成员之一,同样你从submit:取回的应答对象也必须是协议之一:

好了!当你完成之后,别忘了断开连接这样你可以将网络资源还给系统

myLinkToServer disconnect

有趣的Morphic图形系统

教程: 有趣的Morphic图形系统

作者:John Maloney
翻译: @ NirvanaStudio
原文地址:http://static.squeak.org/tutorials/morphic-tutorial-1.html

这篇教程要用在Morphic工程中。要建立一个Morphic工程,只要在Squeak窗口的背景上点击鼠标,不要再任何其他窗口上,出现了屏幕菜单。选择“open….”并选择“project(mophic)”。一个小的桔色的“Unnamed”Morphic工程窗口就出现了。点击窗口中间可以放大到全屏幕。(如果你不确定你在什么地方,只要探出屏幕菜单——如果标题是“World”,那你已经在Morphic工程中了)

首先我们需要建立一个浏览器“Browser”和一个工作区“Workspace”。先复习一下,在Squeak屏幕的空白处按下鼠标左键。在出现的屏幕菜单上选择 ”open…”。点击“browser”。一个绿色的浏览器就出现了。再次利用这个屏幕菜单,选择 ”open…” ,并选择“workspace”。

如果你是一个新手,刚刚看完前面的教程,你应该已经有一个叫“My Stuff”的分类。把浏览器最左边的列表拉到最底端,应该就放在那里。

如果没有,我们需要新建一个地方来添加你的东西。在浏览器的左上面板中,点击右键呼出菜单[Option-click] (在苹果机上 – 教程以下都将Mac苹果的指令以这种方式显示)选择“add item…”并输入“My Stuff”并按回车。把列表拖到最下面,你会发现它已经被选中了。

我们再次白手起家,建立一个程序。我们先从一个空的类开始,这里我们要让他成为Morph的一个子类。在Morphic对象系统中,Morph是一个通用的图形类。我们现在看一下建立新类的一个模版。修改成下面这样:

Morph subclass: #TestMorph
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'My Stuff'

首先选择“Object”并输入单词“Morph”(M一定要大写)。选择“NameOfClass”并输入“TestMorph”。新建立的类就会成为Morph的一个子类。删除实例变量名和类变量名的文字内容。按Alt+s接受。[Command-s]

现在我们来到工作区并新建一个我们刚刚建立的类的实例。输入:

TestMorph new openInWorld.

选择这行并按下Alt-d [Command-d]或者从右键菜单中选择“do It” [Option-click]

一个新的图形对象——一个蓝色的方块,现在出现在左上角。这样你就搞定了一个图形对象,虽然几乎没写什么代码。你可以拾起这个对象并移动它。现在先用鼠标把它抓起来。注意了,当你拾起这个Morphic对象的时候,他下面投射出了一个阴影。这个可以帮助你确定你已经确切的把他抓起来了,而且在其他任何物体之上。如果有对象覆盖在上面,抓起这个对象会自动地把他拉到最前端,这时投影就会显示它现在在其他任何物体之上了。

你可能注意到奇怪的大写方式,但是在Smalltalk中,一个选择器只能是中间没有任何空格的一个字符串。所以我们约定在每个英文单词的第一个字母大写,然后去掉中间的空格连起来。其他语言也常在单词之间使用下划线或者破折号。

halo labels.gif
现在按住Alt并单击 [Command-click]这个蓝色的东西,这时你会看见一组不同颜色的小圈环绕在周围,我们称之为“光环”。这些光环每一个都是向这个Morph对象发送指令的一种快捷的途径。如果我的鼠标停在任意一个圆点上,一个小小的帮助气球会弹出来告诉我这个圆点能做什么。这个黑色的点上的气球说:“Pick up”。也就是,如果我拖动这个黑色的把手,他可以像我直接拖动这个蓝色方框一样。这样可能看起来是一个多余的功能,但是因为可能有时候Morph对象会有交互的行为,比如按钮,这个把手让你可以直接操作一些直接和鼠标动作相关的对象。

这里一个基本的想法是,大多数系统,如HyperCard,依靠模式来改变对象的大小等等。也就是编辑一个对象针对使用它是分离的。在Mophics系统中,你可以在任何东西上面使用光环不用考虑他是如何活动的。光环是在它运行时的一种安全的处理方法。你不需要中止他的按钮功能或者是其他的东西。如果这个东西是一个按钮,你可以无需激活就可以编辑它,也不需要关闭它。

让我们来看看光环到底能做什么。比如这个绿色的,它会产生一个副本。粉红的光环中的X会将它删除(移动到垃圾桶中)。另一个常用的功能是改变它的尺寸,由这个黄色的实现。现在暂时不要对它做其他事情了,尤其不要使用蓝色的按钮(在对象的左下角)。

现在让我们开始制作我们自己的对象吧。我们先写一些方法来自定义它。

我要做的第一件事情是让我们的对象在你用鼠标点击他的时候做点什么。这里我们要实现两个方法。点击浏览器,选中第三个面板中的 “no message”。把下面的代码粘贴到底部的面板并接受,Alt-s[Command-s]

handlesMouseDown: evt
^ true

如果这是我第一次在这个映像中接受一个方法,我必须先输入初始值,这个值将会跟着我修改方法一直记录。这个方法,handlesMouseDown,告诉Morphic对象有一个对鼠标敏感的东西。

下一个方法我们要添加的是当鼠标按下的时候实际要做的事情。继续,把下面的代码粘贴进去成为另一个方法。

mouseDown: evt
self position: self position + (10 @ 0).

将在你点击它的时候,这个代码会让对象向右边移动10个像素。试着点击这个对象看看会发生什么。

在这时我们已经有了两个方法。一个返回“true”说我们需要响应mouseDown:事件。第二个是对鼠标按下事件做出的响应,移动了对象的位置。

现在我想给这个Morph对象一些行为来重新绘制它自己。把这个方法粘贴到浏览器中并用Alt-s接受 [Command-s].

drawOn: aCanvas
| colors |
colors := Color wheel: 10.
colors withIndexDo: [:c :i |
aCanvas fillOval: (self bounds insetBy: self width // 25 * i + 1)
color: c].

现在,当你点击对象的时候,你会看到对象的完全不同的外观。让我们回头看看这个方法究竟做了什么。

这个方法的第三行(代码的第一行)是给临时变量‘colors’赋值。如果你看到两个竖线和一列变量名,那这些变量就是这个方法的局部变量,通常可以叫做临时变量。在Smalltalk和Squeak中,你可以不用自己声明变量——如果确实有系统会自动询问你。而且我们也没有任何“类型”所以你不用声明任何类型。任何变量都可以存放任何对象。所以我们把表达式(Color wheel: 10)的值赋给‘colors’。要显示这个方法作了什么,选择‘wheel:’并按下Alt-m查看实现器。你会看到有两个实现——选择Color class的那个。

我们在Squeak中经常做的一件事情是在方法之前放上一小段注释。第一行说了‘wheel:’是干什么的。他返回一个thisMany colors的一个集合,thisMany是一个参数。这些颜色会均匀分散在色环上。这是颜色的光谱。

如果很方便可以给出这个方法的一个例子,这也是我们经常做的。这里第二个注释是一个表达式你可以实际运行的。选中引号中的内容并按下Alt-d。在屏幕的最上方出现了一条展开的色环。这时一个在光谱上间隔的一些颜色的集合。(但不是最大饱和也是最大亮度,所以他们不会太刺眼。

在这个世界里面我们可以直接在屏幕上涂鸦,但他之后不会自动清除。所以我使用了屏幕菜单的“restore diplay”菜单项来去掉那些画在屏幕顶部的东西。

了解“wheel:”的含义在这里有点偏离主题了。我们知道现在这是一些颜色的集合。点击标题栏左边的 “X”关闭窗口。

这个方法的下一部分遍历这个颜色集合。

"下面是drawOn方法的一部分:"
colors withIndexDo: [:c :i |
aCanvas fillOval: (selfbounds insetBy: self width // 25 * i + 1)
color: c].

withIndexDo: 消息同时提供了一个颜色元素“c”和一个索引“i”,让我们能跟踪我们在列表中的位置。第一次循环索引会变成1,第二次是2……如此继续,直到10。我们只需要知道我们在列表中的哪个位置并增加缩进,让椭圆每次更小。我们发出消息”fillOval:color:“并在带上加上了括号的参数——一个矩形,比外部的矩形尺寸上缩小了 (((self width)// 25) * i + 1)像素,由(self bounds)返回——第二个参数是一个颜色。作为索引,i增加,缩进也增加,每次返回一个更小的嵌入矩形,每个矩形里面我们都会画一个椭圆并用颜色c填充。

当我们再看这个对象时,我们看见了10条颜色。这个看起来很不错啊,同时由于使用了发送给自身的消息“width”,作为他的一个参数,它就可以知道它实际上有多大。用Alt-点击[Command-click] 呼出光环并拖动黄颜色的点。你就可以改变它的大小了,不管变成什么尺寸,他都会一直重新绘制自身。

我下一个想做的事情是点击他并调用一些动画。既然这样那么第一件要做的事情是建立一系列的点。当我点击这个对象的时候,我希望他会沿着每个点来回移动直到所有的点都走过了。为了这个想法,我需要做些工作

Morph subclass: #TestMorph
instanceVariableNames: 'path'
classVariableNames: ''
poolDictionaries: ''
category: 'My Stuff'

回到浏览器,并点击“instance”按钮。然后会出现TestMorph的类定义。然后点击实例变量名的位置,在单引号中间,并输入“path”。这个会成为一个新的实例变量来保存一个显示我们对象位置的点的列表。现在我要点击名称为 “as yet unclassified”的消息分类。

当我添加了一个实例变量,我要知道它应该初始化为什么内容。当我们给TestMorph定义这个消息:

initialize
super initialize.
path := OrderedCollection new.

这一行代码“super initialize”为什么出现在这里?虽然类TestMorph的定义看起来十分简单,但path并不是他唯一的实例变量。在浏览器的第二个面板中,右键点击出现一个菜单,选择“inst var refs”,你会看见一个列表,里边列出了这个对象从超类继承的其他变量。我这里不想选择其中任何一个。如果我做了,他会把所有用到这个实例变量的方法列出来,不过,你可以看到这里有自类Morph继承的6个实例变量。

“super initialize”给我的超类,Morph类发送了初始化消息。当你给接收器“super”发送消息“initialize”的时候,他其实是发送给“self”,但是要保证是被继承的initialize的版本被调用。

我们的初始化的中心是:“ path := OrderedCollection new”。它将一个空的集合赋值给path。现在当我的TestMorph接收了发送的intialize消息,他首先完成Morph要做的初始化,然后,他再完成它自己的初始化。结果是所有从Morph继承的实例变量包括我新添加的都被初始化了。

我们实际上在这里又有一个约定,一般不会自动给一个新的对象发送“initialize”消息。但是,Morph对象总是给每一个新的Morph实例发送初始化消息。

这个TestMorph对象,现在就在屏幕上,并没有对我们新的代码获得初始化。我们在定义了新的 initialize 之前就创建了他。让我们先把它删除并做一个新的。用Alt-点击 [Command-click]呼出对象的光环。点击里面带个 X 的粉红色圆圈来删除这个对象。他现在被扔进了垃圾筒了。现在我们执行下面的代码来搞一个新的TestMorph(也许这次就可以初始化好了):

TestMorph new openInWorld.

我们现在要当你在我们的对象上点击鼠标的时候,让他做点什么事儿。我们现在已经有了一个“path”集合。把下面的方法复制进去并且接受:

startAnimation
path := OrderedCollection new.
0 to: 9 do: [:i | path add: self position + (0@(10* i))].
path := path, path reversed.
self startStepping.

我们在第一行创建了一个新的 OrderedCollection。在下一行,我们作了10次计算并把表达式的结果加到变量path上。我们保存了相对于目前位置的一些点,”self position”,加上一个我建立的点。建立的点的X坐标是0,“@”(可以建立一个点)后面的,Y左边是i的10倍,“i”是循环的变量,从0到9每次增加1。所以,最终的效果是建立的10个点,Y坐标是从当前坐标加上(0的10倍)到(9的10倍)并把这些新的点添加到path集合中。

接下来一行,我们把path扩展为(path, path reverse)。“reverse”会将任何集合用相反的顺序放置。逗号(“,”)操作符是另一个消息选择器用来连接两个集合。

我们所做的是把一个10个点的列表,从0到90,并且附加上一个列表,从90返回到0。这样我们现在就有20个点,出发并最终回到原来的位置。

该方法的最后一行把我们的Morph对象登记在一个可以持续发送“step”消息的一个引擎中。这是动画引擎的秒针,是动画的心跳。现在我们必须让我们的Morph对象理解step方法:

step
path size > 0 ifTrue: [self position: path removeFirst].

这个方法很容易理解。它说只要在点的列表中还有东西,也就是只要path集合的长度大于零,就移动自己。给自己发送消息“position”,并将从点集合中删除的第一个点作为参数传送进去。他在做每一步的之前检查并确保表中确实有东西可以删除。

从图象上看,它会立刻跳到下一个点。

现在这些东西都已经设置好要准备运行了,但是,他并没有开始动画。好吧,那是因为我们并没有真正通过向他发送开始动画的消息,所以我们才没有把这个球踢出去。我非得要做的一件事就是要试着找出发送消息给这个对象的途径,其实有一个最方便的方法就是让他自己给自己发消息,当鼠标按下的时候。

mouseDown: evt
self startAnimation.

这里有两种途径可以定义这个方法。你可以在任何现有的方法上粘贴(在浏览器下面的面板中),或者你可以点击“mouseDown”在右边的面板中,并修改我们现有的mouseDown。其实当你接受的时候都是一样的。

当你完成了上面的,再去点击你的Morph对象,你应该能看到发生了一些东西,它动了,太好了~,但是,他动得十分十分十分得慢。哎~,我失望了因为我以为Sqeak是很快的。其实呢,是默认仅仅每秒钟才给对象发送一次“step”消息.但是对象可以决定它自己需要多久被通知要执行“step”,我们只要实现一个消息“stepTime”:

stepTime
^ 50

它返回对象要求的 step 消息的时间间隔(以毫秒计)。现在,明显你可以要求两次 step 消息之间是 0 秒,但是你不会成功。这里存在一个基本的判断。

现在如果我们再次点击我们的对象,我们就能看到一个快得多的动画。我现在把它改成 10 毫秒,看看能不能达到每秒 100 帧。让我们看看能不能成功。好的,我不知道我是不是真的达到了 100 帧每秒,不过他看上去确实很快。

你知道,我是给“动画公司”工作的。这个动画起始并立刻停止,同时每个人都知道还有“渐进”和“慢出”来让他看上去更优雅。这里有一个小小的改进,使用了“i”的平方来让动画开始的时候比较慢。

startAnimation
path := OrderedCollection new.
0 to: 29 do: [:i | path add: self position + (0@(isquared / 5.0))].
path := path, path reversed.
self startStepping.

我只更改了这个方法里面的一行。我们现在准备在path中建立30个点,然后附加上反转的,一共60个点,另一个需要更改的是替代10次。最后,表达式将会是“841 / 5”。

现在动画就是开始下落时比较慢并且加速然后在他回来时又慢下来。事实上,他看上去像某种球在弹跳。

现在是一个很好机会让你自己和预先制作的Morphic对象玩玩乐。我们先拿一个装满对象的容器。如果你的系统中启用了标签,点击屏幕底部的 “Supplies”标签。如果底部没有标签页,使用屏幕菜单并选择“authoring tools…”。选择“standard parts bin”。无论什么情况你都能获得一大帮对象你可以拖到屏幕上,在你拖出来一个之后,Alt-点击[Command-click] 呼出光环,并按住红色圆圈呼出对象的内容菜单(contents menu)。这里是一些Morph对象:

  • A RectangleMorph (内容菜单中, 试试 “raised bevel” 和 “insetbevel”)

  • An EllipseMorph

  • A StarMorph

  • A CurveMorph (内容菜单中, 试试 “show handles”)

  • A PolygonMorph (f内容菜单中 , 试试”show handles”)

  • A TextMorph (可以用黄色的把手把文字块加宽)

  • [ignore the gradient filled Sketch Morph for now]

  • [ignore the green PasteUpMorph]

  • 画家的调色板可以在你丢下的位置开始新绘制一幅图。画完后,点击“Keep”按钮把图案变成Morph对象

  • [ignore the StringMorph]

简易Smalltalk测试框架

简易Smalltalk测试框架:
以及模式

Kent Beck,
First Class Software, Inc.
KentBeck@compuserve.com

翻译:ShiningRay @ NirvanaStudio

本软件和文档是由编码社区提供的服务。你可以随意分发。First Class Software, Inc.不提供任何形式的声明或隐含的担保。

(Transcribed to HTML by Ron Jeffries. The software is available for many Smalltalks, and for C++, on my FTP site.)

介绍

Smalltalk语言曾饱受煎熬,因为他一直缺少一种测试的习俗。这篇专栏将介绍一种简单的测试策略和框架来支持我们的Smalltalk。虽然这个测试策略和框架并不打算发展成为一个全套的解决方案,但是他相当于一个起点,任何有工业强度的工具和过程都都可以从他起进行建造。
本论文分为三个部分:

  • 原理 – 描述了书写和运行由框架体现的测试原理。阅读这一章了解基本的背景。
  • 手册 – 一个简单模式系统可以用来写你自己的测试。
  • 框架 – 测试框架的一个简单版本。阅读这一章可以深入了解框架是如何操作的。
  • 例子 – 一个使用测试框架来测试Set对象中方法的例子。

原理

我不喜欢基于用户界面的测试。在我的印象中,基于用户界面脚本的测试是十分脆弱的,难于使用。当我还在参加一个项目,我们使用了 用户界面测试,每天早上送来的测试报告中有二三十个测试失败,那是常有的。随便检查一下就会发现大多数的失败中实际上程序还是按照期望的运行的。界面上稍作一些修饰,就会导致最终的输出和期望的输出不一样。我们的测试元花费太多的时间保持测试代码和现有代码同步并且找出那些“伪失败”和“伪成功”,却没多 少时间写新的测试。

我的解决方法是直接在Smalltalk中写测试代码并检查他的结果。这个方法有一个缺点就是你的测试员必须会写一些简单的Smalltalk程序,但因此测试就变得更为可靠。

失败和错误

这个测试框架会区分失败和错误。一个失败指预期可能发生的问题。当你写测试时,你要检查预期的结果。如果你后来得到的是一个不同的结果,这是一个失败的测试。有一个错误那就更为悲惨,你可能根本没检查到有这个错误情况的可能。

测试单元

我推荐开发人员自己写自己的单元测试,每个类写一个。测试框架也支持写测试套件(TestSuite,或叫测试序列),也可以附加给一个类。我还推荐所有的类响应“testSuite”消息,返回一个包含单元测试的测试套件。另外,我还推荐开发人员花20~50%的时间开发测试代码。

集成测试

我推荐由一个单独的测试员写集成测试。哪里运行集成测试呢?最近的用户界面框架运动到更好的程序访问提供了同一个答案——驱动用户界面,但让测试来做。在VisualWorks(下面的实现也会用到这个方言),你可以打开一个应用程序模型ApplicationModel并不断往他的ValueHolders中填入值,造成各种混乱,也不会产什么麻烦。

运行测试

这是原理部分的最后一点。有种做法很有诱惑力——建立一系列测试数据,然后运行一系列测试,然后清除。在我的经历中,这个要比他代来的价值造成更多的问题。一个测试可能会结束和另一个的交互,而且一个测试中的失败可能造成后续的测试都不能够运行。测试框架让我们可以很容易的建立一系列通用测试数据,但是这些数据会在每次测试之前建立并在之后抛弃。这种做法的潜在的性能问题不会造成很大的影响因为测试可以在无人值守的情况下运行。

手册

这里是一个书写测试的简易模式系统。有以下模式:

模式

目的

装置

建立一个通用的测试装置。

测试案例

建立一个测试案例的激发器。

检查

检查测试案例的结果。

测试套件

聚合TestCase测试案例。

装置(Fixture)

你怎样开始写测试呢?

测试是一种不可能的任务。你要让他完全通过,这样你才能保证软件可以正常工作。从另一角度看,由于你的程序的可能状态太庞大,以至于你不可能测试每一种组合情况。

如果你一开始很茫然,都不知道你要测试什么,你就永远不能进入测试的状态。由一个行为可预测的配置开始则要好很多。当你对你开发的软件有更多的了解,你可以加入一个配置列表。

这样一个配置,称之为“装置”(Fixture)。下面是装置的例子:

装置

预计反馈

1.0 and 2.0

算数问题很容易给出预计的答案

到已知的机器的网络连接

对网络包进行相应

#() and #(1 2 3)

发送测试消息的结构

选择一个装置你判断哪些要测试哪些不要。一个完整的测试会有很多装置,每一个都可以以多种途径进行测试。

设计一个测试装置。

  • 创建 TestCase 的子类
  • 在装置中给每个已知的对象添加一个实例变量
  • 重写 setUp 来初始化变量

在下面的例子中,测试装置是两个Set(集合),一个为空另一个包含元素。首先我们给TestCase建立子类并给对象添加实例变量,以便我们以后进行引用:

Test Case测试案例

你已经有了一个装置,下面要做什么呢?

你如何表现测试的一个单元?

你可以预测给装置发出消息之后的结果。你需要通过某种方式表现这样一种预知的情况。

最简单的方法是用交互的方法表现。你打开装置的一个检查器(Smalltalk中的Inspector)
并且给他发送消息。这种做法有两个缺点。第一,你要一直给同一个装置发送消息。如果有一个测试刚巧把对象弄乱了,那么后面所有的测试都会失败,即使代码也许是正确的。更重要的是,你不能方便地和其他人进行交流,交换你的交互测试。如果你把你的对象给了其他人,他们唯一可以测试这些对象的办法就是把你叫来并检查他们。

通过将每一个可预测的情况表现为一个对象,每一个都有一个自己装置,没有哪两个测试会互相干扰。同时,你可以很方便地把测试给别人运行。

用一个方法来代表装置的一个可预见的反应。

  • 给TestCase的子类添加一个方法
  • 用该方法激活装置

下面的例子代码演示了这两点。我们可以预见:将“5”加入一个空的集合会得到“5”在集合中的结果。我们给我们的TestCase的子类添加一个方法。用它来激活装置:

一旦你激活了装置,你需要添加一个检查机制来确保你的预期正确发生。

检查

一个测试案例会激发一个测试装置。

你如何测试要得到预期的结果?

如果你在进行交互测试,你可以直接检查预期的结果。如果你要查看一个特定的返回之,就是用“print it”,并确保你获得正确的对象。如果你要查看一些额外的影响,请使用Inspector检查器。

由于测试是在他们自己的对象中,你需要一种直接用程序查找问题的方法。一种实现办法是使用标准错误处理机制(Object>>error:),以测试逻辑为error:信号

当你在测试的时候,你可能想要区别你要检查的错误,象二加三得六,和那些你没有预见的错误,如下标越界或者是未定义的消息。

你还没什么办法对付无法预见的错误(如果你已经对他们作了些什么,他们就不会说无法预见了,是不是?)当一个灾难性的错误降临时,测试框架会停止运行测试案例,记录错误,并直接运行下一个测试案例。由于每一个测试案例都有他自己的装置,前一个案例中的错误不会影响下一个。

测试框架简单的提供了一个“should:”方法来检测预期的值,这个方法带一个语句块作为参数。如果语句块得出的是真,那一切都好。否则,测试案例停止运行,测试失败被记录在案,然后运行下一个测试案例。

把检查的内容变成一个返回值为布尔型的语句块,并把语句块作为参数传递给“should:”。

在下面的例子中,在通过给一个空集合加“5”激活装置之后,我们希望检测并确保里面包含它。

有一个TestCase>>should: 的变体。TestCase>>shouldnt: 在语句块参数为真的情况下失败。这样你就不必使用“(…)not”了。

你一旦有了一个测试案例,你就可以运行它了。给你的TestCase的子类创建一个实例,并给出测试方法的选择器。并发送“runt”给结果对象:

如果他运行结束,那么测试通过了。如果摔了个跟头,看来有什么东西出错了。

TestSuite测试套件

你现在有若干测试案例。

你如何一下运行许多测试呢?

一旦你有两个测试案例在运行,你会希望他们能一个接一个运行而不是你去执行两次“do it”。你可以就把两块运行表达式放弃及然后执行来运行测试。然而,如果当你又需要运行“这一块案例和那一块案例”的时候呢,你就会烦了。

测试框架提供了一个对象来表示“一系列测试”——测试套件TestSuite。一个TestSuite运行一套测试案例并且把他们的运行结果同时汇报上来。根据多态的优点,TestSuite同样可以包含其他TestSuite,所以你可以把Joe的测试和Tammy的测试放在一个创建一个更高一级的套件。

把测试案例组合到测试套件中。

给一个TestSuite发送了“run”的结果是一个TestResult对象。他记录了所有的测试案例引发的失败或错误,和套件运行的时间。

所有这些对象都是可以通过ObjectFiler或者BOSS进行储存。你可以很方便的保存一个套件,然后调入并运行,和以前的结果进行比较。

Framework框架

这一部分展示了测试框架的代码。如果你对框架的实现很好奇,或者你想要修改它,那么就在这一章了。当你和一个测试员交谈时,他们谈论的测试的最小单元是测试案例。TestCase是一个用户的对象,代表了一个单个测试案例。

测试员谈论如何设置一个“测试装置”,这是一个带有可预测响应结构的对象,他既容易建立也容易推导。几个不同的测试案例可以针对同一个测试装置。

这个差别表现在框架中是通过给每个TestCase一个可插入的选择器。选择器调用的变量行为便是测试代码。同一个类的所有实例共享同一个装置。

TestCase class>>selector: 是一个 Complete Creation Method

TestCase>>setSelector: 是 Creation Parameter Method

TestCase的子类需要相应改写钩子方法setUp和tearDown来建立和销毁测试装置。TestCase本身提供了两个什么也不做的基本方法。

运行一个TestCase最简便的方法就是向他发送“run”消息。Run调用设置代码,运行选择器,然后运行拆卸代码。注意拆卸代码不管执行测试的时候有没有错误都会执行。调用setUp和tearDown可以封装成Execute Around Method,不过由于他们不是公共接口的一部分,他们必须在这里编写。

PerformTest 仅仅执行一下选择器

单个的TestCase一点意思也没有,一旦你让他运行起来了之后。然后,你就会想一次运行很多很多测试案例。测试员谈论运行测试“套件”。TestSuite是一个用户对象。它是测试案例的组合。

TestSuites 是有名称得对象。因此可以很方便判断他们的身份,这样他们可以被储存在存储器中,也可以从中读取。一下是完整的构造方法和构造参数方法。

testCases实例变量是在TestSuite>>setName: 中初始化的因为我不需要让他变成其他类型的集合。

测试套件有一个名称的访问方法,这样可以在用户界面上显示他们。

当你行一个TestSuite,你可能想要运行它所有的TestCase。但他不是这样简单的。如果你有一个代表应用程序的验收测试侧套件,在运行之后,你可能还想知道套件运行了多久,哪个案例有问题。这些是你可能想要储存以便将来引用的信息。

TestResult是一个TestSuite的结果对象。运行一个TestSuite返回一个TestResult,记录了上面描述的信息——起始时间和中止时间,套件名称和所有的失败和错误。

TestCase>>run 和 TestSuite>>run 在多态上并不相同。这是需要在以后的框架中解决的问题。一种做法是用一个可以按微秒度量的TestCaseResult来做性能回退测试(performance regression testing.)。

默认的TestResult由TestSuite构造,使用一个 Default Class.

一个TestResult Complete Creation Method 需要一个 TestSuite.

TestResult可以通过发送start和stop消息来做时间标记。由于start和stop需要成对执行,他们必须隐藏在一个Execute Around Method方法中。这也是以后要做的。

当一个TestSuite运行时给出一个TestResult,它仅仅将其中每一个TestCase带着TestResult运行。

#run: 是TestSuite和TestCase中的组合选择器,这样你可以构造包含其他TestSuite的TestSuite,替代或者补充TestCase。

当一个TestCase运行时给出了一个TestResult,它应该要么安静无误得运行,向TestResult中添加一个错误,或者添加一个失败。捕获错误简单的使用了系统提供的errorSignal。捕获失败必须由TestCase本身提供。首先,我们需要一个 Class Initialization Method 来创建一个信号。

现在我们需要一个 Accessing Method.

现在,当TestCase用一个TestResult运行时,它要捕获错误和失败然后通知TestResult,同时它必须运行tearDown代码不管测试是否正确执行。者造成了这是框架中最丑的方法,因为有两个嵌套的错误处理器和方法中的valueNowOrOnUnwindDo:。这里少解释了一个模式在TestCase>>run 关于使用ensure:确保安全运行Execute Around Method 的第二个停止。

当一个TestResult被告知有一个错误或者失败发生了,他在他其中一个集合中记录这个事实。为了简单起见,记录只是两个数组,但是他可能应该是一个类对象包含一个时间标签和问题更详细的信息。

一旦在测试方法中出现一个未捕获的错误(例如,无法辨认的消息),就调用错误的情况。失败的情况如何调用呢?TestCase提供了两个方法简化了失败检查。第一个是should: aBlock,如果aBlock执行后返回假便发出失败消息。第二个,shouldnt: aBlock,和前面刚好相反。

测试方法将会运行代码来激发测试装置,然后在should:shouldnt:块中检查结果。

例子

Ok, 这就是他的工作原理,你怎么来用它呢?这里有一个简短的例子测试Set支持的一些消息。首先我们建立TestCase的子类,因为我们总想有好几对有趣的集合可玩。

现在我们需要初始化这些变量,所以我们重写setUp。

现在我们需要一个测试方法。让我们测试看看给一个集合添加元素究竟能否成功。

现在我们可以通过执行”(SetTestCase selector: #testAdd) run”来运行一个测试案例。

这里有一个使用shouldnt:的情况。他这么念的”after removing 5 from full, full should include #abc and it shouldn’t include 5.”

这里又有一个特殊情况,确保在你要使用索引访问的时候发出错误信号。

现在我们可以把它们一起放在一个测试套件里面。

这里是这个套件的对象浏览器图TestResult是我们运行之后得到的。 

以上所展示的测试方法仅仅覆盖了Set中的一小部分功能。给Set中所有的公共方法写测试确实是一个很让人郁闷的任务。尽管如此,就像 Hal Hildebrand 在使用了这个框架的早期一个版本之后告诉我的那样。“如果根本的对象不能正常工作,其他一切都免提。你必须写测试来确保所有的东西都能正常工作!”

Smalltalk不好吗?

原文:http://www.cincomsmalltalk.com/blog/blogView?showComments=true&entry=3295067130
by James Robertson
翻译:ShiningRay

我收到了一封邮件,看到了一个语言趋势的帖子的链接——这个帖子说大约在1995年,Smalltalk有一个很好的及正派的用途,但是在2005却被大家扔进了墙角。他问到:

那么为什么会导致这种情况呢?这个更大的故事的背后:对于Smalltalk的否定。我很感兴趣的是,为什么Smalltalk会从他的顶点——1995年OO语言的首选落到到它现在作为一个强大却没人用的平台的地步。这是否也是Java的未来呢?如果我能更好的理解Smalltalk的衰落(更不用提Objective C了),我可以更好地预期Java和其他语言的未来。

好吧,有两件事情影响了Smalltalk的使用——ParcPlace-Digitalk(也就是后来的ObjectShare)的行为和IBM的行为。先让我说一下PPD/OBJS。ParcPlace在1995年是一个快速成长的公司,但那在与Digitalk合并之前。之后这个公司就逐渐显现出一些组织上的劳损——管理小组充其量只是次好的(他们所做的与Digitalk合并的决定就很好的说明了这个问题)。

那次合并消耗了接下去的18个月和大量资金。公司不仅仅花了18个月尝试将VisualWorks和VSE合并,他还:

  • 烧掉了很多钱却带来了几个没有效果的新东西
  • 在尝试代码合并的过程中没有发布任何VW或者是VSE的新版本
  • 由于没有去管理合并本身的事宜,结果导致了在工程和咨询小组之间致命的冲突。

不用说了,这种事情根本不能给客户群带来任何信息。这已经够坏了——然后财政问题的谣言就传开了,然后再1997年的动态年管理层发布了VSE的EOL(不包括任何迁移策略)的声明,VW/VSE的代码合并终止,以及一个新的Java策略。然后VSE的客户群就乱套了,已经对不存在的新发布很紧张,质问在新的Java的关注下,到底这个产品还能获得多少关注。

一切就这样进行直到新的管理团队进入并把公司名称更改为了ObjectShare(在原来管理层的疯狂的收购行为中一个被培养出来的公司)。事情一团糟,Java产品砰一下砸了出来,VW也爬了出来。新的管理层尝试扶持股票价格但是未能成功,情况又继续下滑(这一次又没有对顾客和前景带来任何信心)。最后,Cincom在1999年收购了Smalltalk业务(从此事情在我们这边开始有了转机)。

。下面,你要看一下Java的介绍和IBM的举动。在90年代中期的时候,IBM有一个很成功的Smalltalk产品,而且他们所有的开发环境都是基于它的——从VisualAge Smalltalk产生了一个C++、Cobol、同时最后,一个Java工具集。IBM的这些东西做的都很好,并且已经设法(由于PPD刚刚抛弃了他们)。然而,Java得到了很多绯闻,而且IBM对于构建VA架构基础花了很多他们自己的钱。提醒一下,这部分是我的推测,不过我认为发生了这些事情:

  • IBM发现Java在快速流行起来。
  • 他们认识到直接骑在Sun的Java上要比维护他们自己的一套基于VAST的开发工具要便宜得多

后一个观点最后因为Sun免费发布Java而成真。同时这两个举动都帮助了Java惊人地增长,我一点也不确定他们是否帮助了Sun一点点。这是另外一个故事了——在Smalltalk的世界中,IBM逐渐降低了他们对Smalltalk的投资(最后以他们把这个技术推给Instantiations而告终,今年),同时开始快速书写他们的Java故事。这也没有给Smalltalk用户或者是对Smalltalk的期望带来任何信息——特别是当IBM的销售人员们开始干劲十足地推进他们的Java生产线和驳倒Smalltalk的时候。

总之——ParcPlace(以及他后来的公司,PPD/OBJS)自己把自己玩完了,破坏了它原有的客户群,并让潜在的用户对Smalltalk的信息也降低了。同时,IBM开始从Smalltalk转移至Java上——也产生了同样的效果。在90年代中期其他的Smalltalk提供商还过于微小无法影响这种不断成长的观点病毒——Smalltalk正在消亡。

而现在不同了——从1999末开始,Cincom勤奋地研究Smalltalk,我们也拥有一个快速增长的、并且盈利的业务。同时还有很多其他的Smalltalk方言,包括现在要称为VA Smalltalk的。Smalltalk在当前并不是主流,但是它还是被很多期望一个有生产力的候选工具的公司所使用。

SmaCC 指南

这是一个用于演示一些SmaCC(Smalltalk编译器的编译器)的简要指南。在这个例子中,我们会逐步开发一个简易的计算器。

如果你已经做过这种东西,你可以先 载入代码 。你载入了代码之后,你需要打开SmaCC解释器生成器。在VisualWorks 和 VisualAge中,它在Tools菜单下。Dolphin的在一个额外的工具目录中。它会打开一个类似下面的窗口:

SmaCC Window

我们第一个计算器相对比较简单。它只要能读取两个数字并把它们相加。开始之前,我们首先要告诉扫描程序如何辨认一个数字。数字由一个或多个数字打头,后面可能还有一个小数点加上0或者更多的数字。扫描程序对这个标记的定义是:

把这行代码输入界面上的scanner标签页中。让我们逐个看每一个部分:

<number>

指出记号的名字。在<>中的名称必须是合法的Smalltalk变量名。

:

分隔记号名称和记号定义。

[0-9]

匹配任何一个在’0’到’9’(一个数字)范围中的字符。

+

匹配前面的表达式一次或多次。在这种情况下,我们要匹配一个或多个数字。

( … )

标示子表达式组。

\.

匹配 ‘.’ 字符(. 在正则表达式中有特殊的含义,使用 \ 来转义)。

*

匹配前一个表达式零次或多次。

?

匹配前面的表达式零次或一次。(也就是,前面表达式是可选的)。

;

终止一个记号说明。

我们不想去关心我们语言中的空白符,所以我们需要定义什么是空白符并且忽略它,输入下面的记号说明:

\s 会匹配任何空白字符(空格、制表符、换行、回车等等)。然后我们怎么告诉扫描程序去忽略它呢?
如果你看一下SmaCCScanner类,你会发现一个叫做’whitespace’的方法。如果一个扫描程序有一个方法的名称和某个标记一样,那么一旦扫描程序匹配了这类标记就会调用这个方法。正如你所见,whitespace方法会吃掉空白符。同样还有一个’comment’方法会作类似的处理。

说到我们的语法,现在让我们来定义它吧。在Parser表格中输入以下语法说明:

这基本上指出了一个表达式可以是一个数字或者是另一个表达式加上一个数字。

我们现在应该可以编译一个分析器了。切换到Compile标签页。你要输入扫描器和分析器的类的名称。这里我们相应使用CalculatorScanner 和CalculatorParser。当类名输完之后,我们就准备编译分析器了。点击 ‘Compile LARLR(1)’
按钮(你应该总是点这个按钮除非你知道你要做什么。一般来说,他会生成比另一个选项更小的分析器)。这时就会生成新的CalculatorScanner和CalculatorParser的Smalltalk类同时会编译这两个类中的一些方法。所有的SmaCC编译出来的方法会按照”generated-*”的格式。你不可以更改这些方法因为每次你重新编译他们都会被覆盖。

不管SmaCC何时创建新类,这些类都会被放到默认的应用程序/包中。如果你使用的是VisualAge,你要确保默认应用程序是开放的版本而且SmaCCRuntion应用程序已经安装(prereq)。

如果你已经生成了扫描器和分析器类,你可以通过类名旁边的”…”按钮来载入他们的定义。如果在出现的对话框中你回答”Yes”,那么在Scanner/Parser标签页中的文本就会被替换为上一次编译过的定义(假设”Generate definition comments”在上一次编译中被选中了)。

现在我们要测试我们的分析器了。进入“test”面板,输入“ 3 + 4”(不要加双引号),并且点击“parse”按钮;你会看到分析器正确分析了它。如果你点击“Parse and Inspect”你会看到一个检视器(inspector),里面有一个包含了被解析的记号的顺序集合(OrderedCollection)。这是因为我们没有指明当分析器在解析的时候要怎么处理记号。你也可以是如一个不正确的内容。例如尝试解析“3 + + 4”或者“3 + a”。应该会出现一个错误信息。

现在我们要定义当我们分析我们的表达式的时候要产生的动作。当前的情况是,我们的分析器仅仅验证表达式是一些相加的数字。一般来说你要创建一些结果来表示你已经解析了什么内容(比如,一个棵分析树)。然而,在这个情况下,我们不关心结构,我们只关心结果(表达式的值)。在我们的例子中,你需要把语法定义改成如下:

括号中的文本是Smalltalk代码,当应用规则的时候,就会执行这个代码。有一个数字的字符串会被替换成相应的解析节点。在第一个Expression的规则中,’1’会被替换成匹配Expression的ParseNode同时’3’会被替换成匹配Number的ParseNode。在规则中的第二个东西是’+’记号。因为我们已经知道它是什么了,所以我们对他不感兴趣。编译新的分析器。现在当你从Test面板中执行’Parse and Inspect’时,你应该看到这个结果:7。

前面的代码有一个问题是如果你需要更改一个规则,你可能也要跟着修改规则内的代码。例如,假设你你在规则的开头添加了一个新的记号,那么你就要更改所有在Smalltalk代码中的引用了。我们可以通过使用命名表达式来减少这种问题。在规则的每个部分后面,我们可以指明它的名称。名称是通过单引号来标明的,它同样必须是一个合法的Smalltalk变量名。象下面这个:

它和前面解析的语言的结果是一样的,但它同时让你更容易维护的你分析器。让我们现在扩展我们的语言并加入减法功能。这里是新的语法:

你编译了这个代码之后,’3 + 4 – 2’就会返回’5’了。下面,再让我们加入乘法和减法:

这时我们遇到一个问题.如果你计算” 2 + 3 * 4“,最后的结果将是 20。这个问题是因为在标准的数学中,乘法比加法有更高的优先级。我们的语法是严格按照从左到右的方式运算的。这个问题一般的解决方法是定义加法的非终结符来强制计算的顺序。这个解决方法的语法类似:

这时候如果你编译这个语法,你会看到” 2 + 3 * 4 “的计算结果是14,正如我们所期望的那样。现在,正如你可以想象的,当优先级规则的数量增加时,语法也越来越复杂(例如,C语言)。我们可以使用歧义语法和优先级规则来简化这种情况。这里有一段使用优先级来限制计算顺序的一段语法:

注意我们更改了语法所以操作符两边都是Expression。我们在语法顶部添加的两行表示“+”和“-”是从左至右运算的而且优先级相同,同时他们的优先级比“*”和“/”低。类似的,第二行表示“*”和“/”有同样的优先级。这个形式的语法通常更加直观,特别是当有很多优先级要处理的时候。我们再来一个例子,现在加入指数运算和括号:

当你编译了这个语法之后,你就可以正确计算” 3 + 4 * 5 ^ 2 ^ 2“得到2503了。由于这个指数操作是右结合的,所以这个表达式是象这样计算的3 + (4 * (5 ^ (2 ^ 2)))。我们也可以计算带括号的表达式。例如,计算 ” (3 + 4) * (5 – 2) ^ 3 “将得到189。