测试运行

使用 IronPython 进行请求-响应测试

James McCaffrey

下载代码示例

我比较喜欢使用 Python 编程语言来执行多种类型的小型测试自动化任务。在本月的专栏中,我会向大家介绍如何使用 IronPython(与 Microsoft .NET Framework 兼容的 Python 实现)对 ASP.NET Web 应用程序执行 HTTP 请求-响应测试。

具体而言,我会创建一个简短的测试工具脚本,用于模拟用户执行 ASP.NET 应用程序。IronPython 工具以编程方式将 HTTP 请求信息发布到 Web 服务器上的应用程序。然后,会提取 HTTP 响应流
并检查 HTML 文本是否为某种预期值,从而确定结果是通过还是失败。除了其本身是一项实用的
测试技术外,了解如何使用 IronPython 执行 HTTP 请求-响应测试也是了解 IronPython 语言的最佳途径。

本专栏假设您对 ASP.NET 技术已有一些基本了解,具备使用 JavaScript、Windows PowerShell、VBScript、Perl、PHP 或 Ruby 等语言编写脚本的中级技能,并且假设您没有使用 Python 的任何经验。然而,即使您不熟悉 ASP.NET 和脚本编写,也应当仍然能够轻松理解本专栏介绍的内容。若要了解我将介绍的内容,最好是看一下图 1图 2 中的屏幕快照。

图 1 说明了待测试的示例 ASP.NET Web 应用程序。待测试系统是一个名为 MiniCalc 的简单但极具代表性的 Web
应用程序。我有意使待测试的 ASP.NET Web 应用程序尽可能简单,以免 IronPython 测试自动化中的要点变得模糊。实际的 Web 应用程序要比图 1 中所示的虚拟 MiniCalc 应用程序复杂得多,但此处所介绍的 IronPython 测试技术可轻松推广到复杂的应用程序。MiniCalc Web 应用程序接受两个数值、将值相加或相乘的指示,以及答案显示的小数位数。然后,将这些值发送到用于计算结果的 Web 服务器。该服务器会创建 HTML 响应并将其发送回客户端浏览器,其中显示了具有用户指定的小数位数的结果。

图 1 待测试的 MiniCalc Web 应用程序

图 2 显示的是正在运行的 IronPython 测试工具。我的脚本名为 harness.py 并且不接受任何命令行参数。为简单起见,我使用硬编码处理了 Web 应用程序的 URL 以及测试用例输入文件名等信息。工具首先会回显目标 URL:http://localhost/MiniCalc/Default.aspx。在测试用例 001 中,工具通过编程方式发布信息,相当于用户将 1.23 键入控件 TextBox1,将 4.56 键入 TextBox2,选择 RadioButton1(表示加法操作),从 DropDownList1 控件中选择 4,然后单击 Button1(执行计算)。工具会在后台捕获来自 Web 服务器的 HTTP 响应,然后搜索响应中是否指示 TextBox3 结果控件中显示的是 5.7900(1.23 与 4.56 的正确总和并保留四位小数)。工具会记录通过的测试用例数和失败的测试用例数,并在处理所有测试用例之后显示这些结果。

图 2 使用 IronPython 测试应用程序

在后续部分中,我将简要介绍一下待测试的 Web 应用程序,从而使您准确地了解测试内容。接下来,将向您介绍使用 IronPython 创建小型 HTTP 请求–响应自动化程序的详细信息。最后,作为总结,我会提出几条有关何时适合使用 IronPython 以及何时更适合使用其他技术的意见。我相信您会发现这些信息对于您的测试工具集是个有趣且有用的补充。

待测试的应用程序

我们来看看作为测试自动化目标的 MiniCalc ASP.NET Web 应用程序的代码。我使用了 Visual Studio 2008 来创建应用程序。在以管理员权限启动 Visual Studio 后,单击“文件”|“新建”|“网站”。为了避免使用 ASP.NET 代码隐藏机制并且在一个文件中保存 Web 应用程序的所有代码,我选择了“空网站”选项。然后,在位置字段中指定了 http://localhost/MiniCalc。我决定对 MiniCalc 应用程序使用 C# 语言,但本专栏中介绍的测试工具适用于以 VB.NET 编写的 ASP.NET 应用程序;只要稍作修改,该工具便可以针对以传统的 ASP、CGI、PHP、JSP 和 Ruby 等技术编写的 Web 应用程序。在“新建网站”对话框上单击“确定”,以生成 Web 应用程序的结构。接下来,转到解决方案资源管理器窗口,右键单击 MiniCalc 项目名称,并从上下文菜单中选择“添加新项”。然后,从已安装的模板列表中选择“Web 窗体”,并接受 Default.aspx 文件名。我清除了“将代码放在单独的文件中”选项,然后单击“添加”按钮。接下来,在解决方案资源管理器中双击 Default.aspx 文件名,以编辑模板生成的代码。我删除了所有模板代码并替换为图 3 中所示的代码。

图 3 MiniCalc 代码

<%@ Page Language="C#" %>
<script runat="server">
  private void Button1_Click(object sender, System.EventArgs e)
  {
    double alpha = double.Parse(TextBox1.Text.Trim());
    double beta = double.Parse(TextBox2.Text.Trim());

    string formatting = "F" + DropDownList1.SelectedValue;

    if (RadioButton1.Checked)
      TextBox3.Text = Sum(alpha, beta).ToString(formatting);
    else if (RadioButton2.Checked)
      TextBox3.Text = Product(alpha, beta).ToString(formatting);
    else
     TextBox3.Text = "Select method";
  }
  private static double Sum(double a, double b)
  {
    return a + b;
  }
  private static double Product(double a, double b)
  {
    return a * b;
  }
</script>

<html>
  <head>
    <style type="text/css">
     fieldset { width: 16em }
     body { font-size: 10pt; font-family: Arial }
    </style>
    <title>Default.aspx</title>
    </head>
  <body bgColor="#ccffff">
    <h3>MiniCalc by ASP.NET</h3>
    <form method="post" name="theForm" id="theForm" runat="server"             action="Default.aspx">
      <p><asp:Label id="Label1" runat="server">Enter number:&nbsp</asp:Label>
      <asp:TextBox id="TextBox1" width="100" runat="server" /></p>
      <p><asp:Label id="Label2" runat="server">Enter another:&nbsp</asp:Label>
      <asp:TextBox id="TextBox2" width="100"  runat="server" /></p>
      <p></p>
      <fieldset>
        <legend>Arithmentic Operation</legend>
        <p><asp:RadioButton id="RadioButton1" GroupName="Operation"                 runat="server"/>Addition</p>
        <p><asp:RadioButton id="RadioButton2" GroupName="Operation"                 runat="server"/>Multiplication</p>
        <p></p>
      </fieldset>
    <p>Decimals:   
      <asp:DropDownList ID="DropDownList1" runat="server">
      <asp:ListItem>3</asp:ListItem>
      <asp:ListItem>4</asp:ListItem>
      </asp:DropDownList>
    </p>
      <p><asp:Button id="Button1" runat="server" text=" Calculate "            onclick="Button1_Click" /></p>
      <p><asp:TextBox id="TextBox3" width="120"  runat="server" />
    </form>
  </body>
</html>

为使源代码规模变小并易于理解,我采用了一些捷径,例如不执行错误检查,以及将服务器端控件(如 <asp:TextBox>)与纯 HTML(如 <fieldset>)进行组合。 图 3 中代码的最重要部分是 ASP.NET 服务器端控件的 ID。 我使用的是默认 ID:Label1(用户提示)、TextBox1 和 TextBox2(输入两个数字)、RadioButton1 和 RadioButton2(选择加法或乘法)、DropDownList1(结果的小数位数)、Button1(计算)和 TextBox3(结果)。 若要使用此处介绍的技术针对 ASP.NET 应用程序自动执行 HTTP 请求-响应测试,您必须知道应用程序控件的 ID。 在这种情况下,由于是我自己创建的应用程序,因此我可以提供源代码;不过,即使您测试的是他人编写的 Web 应用程序,也可以始终使用 Web 浏览器的“查看源代码”功能来检查应用程序。

为验证待测试的 Web 应用程序是否已正确构建,我按下了 <F5> 键。 在生成的“未启用调试”对话框中单击“确定”,指示 Visual Studio 修改 Web 应用程序的 Web.config 文件。 然后,Visual Studio 会启动 Internet Explorer 并加载 MiniCalc。 请注意,<form> 元素的 action 属性已设为 Default.aspx。 换言之,每次用户提交请求时,都会执行相同的 Default.aspx 页面代码。 这使得 MiniCalc Web 应用程序如同一个应用程序,而不是一系列不同的网页。 由于 HTTP 是无状态协议,因此 ASP.NET 通过将 Web 应用程序的状态放在名为 ViewState 的特殊隐藏值类型中来实现应用程序效果。 正如稍后您将看到的那样,处理 ASP.NET 应用程序的 ViewState 是以编程方式向应用程序发布数据的关键之一。

使用 IronPython 进行 ASP.NET 请求-响应测试

让我们回顾一下生成了图 2 中屏幕快照的 IronPython 测试工具程序。 IronPython 可以从 CodePlex(Microsoft 发起的开源项目)免费下载,网址为 ironpython.codeplex.com。 我使用的是 2.6.1 版,可在 .NET Framework 2.0 版上和支持该版本 Framework 的任何计算机上运行。 图 4 介绍了我的测试工具脚本的整体结构。

图 4 测试工具结构

set up imports
define helper functions

try:
  initialize variables
  open test case data file
  loop
    read a test case from file
    parse test case data
    determine ViewState
    determine EventValidation
    construct request string
    send request string to app
    fetch response from app
    determine if response has expected result
    print pass or fail
  end loop
  close test case data file
  print summary results
except:
  handle exceptions

如您所见,我的工具脚本非常简单,由外部测试用例数据文件驱动。 该测试用例数据文件名为 testCases.txt,其中包含以下内容:

001|1.23|4.56|RadioButton1|4|clicked|5.7900
002|1.23|4.56|RadioButton2|4|clicked|5.7900
003|2.00|3.00|RadioButton1|4|clicked|5.0000

每一行代表一个测试用例,并且以“|”字符分隔七个字段。 第一个字段是测试用例 ID。 第二个和第三个字段是 TextBox1 和 TextBox2 的输入。 第四个字段编码目的是请求加法还是乘法。 第五个字段是小数位数 DropDownList 控件的值。 第六个字段(“clicked”)是 Button1 事件。 第七个字段是预期结果,应显示在 TextBox3 中。 第二个测试用例有意设计的不正确,只是为了演示测试用例失败情况。 对于我在此描述的小型测试方法,使用简单的文本文件保留测试用例数据通常是一种不错的选择。 如果要在工具脚本中直接嵌入测试用例数据,只需使用以下字符串数组即可:

testCases = ['001|1.23|4.56|RadioButton1|4|clicked|5.7900',
             '002|1.23|4.56|RadioButton2|4|clicked|5.7900',
             '003|2.00|3.00|RadioButton1|4|clicked|5.0000']

然后,迭代每个测试用例,如下所示:

for line in testCases:
  ...

Python 还提供了可用于存储测试用例数据的列表类型。

我的 IronPython 测试工具的前三行如下:

# harness.py
import sys
import clr

Python 中的注释以“#”字符开头,并延伸到行尾。 通过“import sys”语句,脚本可以访问特殊 IronPython sys 模块中的资源。

从 IronPython 交互式控制台发出 sys.path 命令,可列出这些资源的位置。 通过“import clr”语句,脚本可以访问和使用核心 .NET CLR 功能。

接下来的六条语句显式启用工具所使用的 .NET 功能:

from System import *
from System.IO import *
from System.Text import *
from System.Net import *
clr.AddReference('System.Web')
from System.Web import *

此处第一行导入 System,与 C# 程序中的“using System”语句类似。 “import clr”语句包括 System 命名空间,因此我可以省略“from System import *”语句,但是我倾向于将它以文档形式保留。 随后的三条语句将 System.IO(用于文件操作)、System.Text(用于字节转换)和 System.Net(用于请求-响应功能)命名空间引入作用域。 “clr.AddReference(‘Sys­tem.Web’)”语句将 System.Web 命名空间引入作用域。 默认情况下无法直接访问此命名空间,因此必须在发出“from System.Web import *”语句之前使用 AddReference 方法,才能访问 URL 编码方法。

接下来,定义一个帮助程序方法来提取待测试的 Web 应用程序的 ViewState 信息:

def getVS(url):
  wc = WebClient()
  bytes = wc.DownloadData(url)
  html = Encoding.ASCII.GetString(bytes)
  start = html.IndexOf('id="__VIEWSTATE"', 0) + 24
  end = html.IndexOf('"', start)
  vs = html.Substring(start, end-start)
  return vs

请记住,由于 HTTP 是无状态协议,因此 ASP.NET 通过将应用程序的状态放在名为 ViewState 的隐藏值中来塑造出有状态应用程序的效果。 ViewState 值是 Base64 编码的字符串,用于在多次请求/响应往返期间保持 ASP.NET 页面的状态。 与此类似,出于安全考虑在 ASP.NET 2.0 中添加了 EventValidation 值,以防止受到脚本插入式攻击。 对于以编程方式向 ASP.NET Web 应用程序发布数据而言,这两个机制非常关键。

在 Python 中,您必须先定义函数,然后才能在脚本中进行调用。 函数是使用 def 关键字定义的。 帮助程序函数首先实例化 WebClient 对象。 接下来,DownloadData 方法向 url 参数指定的 Web 应用程序发送 HTTP 请求,并提取 HTTP 响应作为字节值数组。 我使用 GetString 方法将字节转换为名为 html 的字符串。 ViewState 元素如下所示:

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"
 value="/wEPDwULLTEwNjU=" />

因此,为了提取值,我首先确定子字符串 id="__VIEWSTATE" 的位置,然后添加 24 个字符。 这种方法比较脆弱,因为该技术在 ASP.NET 更改 ViewState 字符串的格式时就会中断;不过,由于这是小型自动化,因此简易性胜过可靠性。 接下来,确定终止双引号字符的位置,然后就可以使用 Substring 方法提取 ViewState 值。 与使用 begin...end 或 {...} 之类标记分隔代码块的大部分语言不同,Python 使用的是缩进。 如果您不熟悉 Python 编程,开始可能会不习惯,但是和我交流过的大部分工程师都坦承可以很快适应这种语法。 许多模块都支持 Python,因此使用 .NET 方法检索 ViewState 值的一种替代方法是在本机 Python urllib 模块中使用函数。

在定义 getVS 帮助程序函数之后,我又定义了一个帮助程序函数以获取 EventValidation 值:

def getEV(url):
  wc = WebClient()
  bytes = wc.DownloadData(url)
  html = Encoding.ASCII.GetString(bytes)
  start = html.IndexOf('id="__EVENTVALIDATION"', 0) + 30
  end = html.IndexOf('"', start)
  ev = html.Substring(start, end-start)
  return ev

提取 EventValidation 与提取 ViewState 所使用的技术相同。 请注意,Python 是动态类型化语言,因此我没有指定参数、变量和对象的数据类型。 例如,DownloadData 返回字节数组,IndexOf 返回整数,IronPython 解释程序会计算这些类型。 定义 getVS 和 getEV 这两个函数要求两次往返调用待测试的 Web 应用程序,因此您可能需要将这两个帮助程序函数组合为一个函数,并将帮助程序命名为 getVSandEV(url) 之类的名称。

在定义帮助程序函数之后,就会开始执行实际自动化:

try:
  print '\nBegin IronPython Request-Response testing'
  url = 'http://localhost/MiniCalc/Default.aspx'
  print '\nURL under test = ' + url + '\n'
  testCases = 'testCases.txt'
  ...

与需要入口点(如 Main 方法)的一些语言不同,Python 脚本执行仅从第一条可执行语句开始。 我使用 try 关键字来捕获任何异常,然后输出启动消息。 Python 允许使用单引号或双引号来定义字符串文本,并且可以在任何一个分隔符中嵌入转义序列(如 \n)。 若要禁用转义序列求值,可以在字符串文本前面加上小写字母 r(“raw”),例如:file = r'\newFile.txt'。 我对待测试的应用程序的 URL 进行了硬编码,并将该值显示给 Shell。 如果要从命令行读取 URL,则可以使用内置的 sys.argv 数组,例如:url = sys.argv[1]。 Python 使用“+”字符来连接字符串。 另外,我还对测试用例数据文件的名称进行了硬编码;由于我未包含文件路径信息,因此假设该文件与 IronPython 脚本位于同一目录。

接下来,设置计数器,打开我的测试用例数据文件并开始迭代该文件:

...
numPass = numFail = 0
fin = open(testCases, 'r')
for line in fin:
  print '==========================================='
  (caseid,value1,value2,operation,decimals,action,expected) = 
    line.split('|')
  ...

Python 拥有多种我非常喜欢的特性,包括多变量赋值和简明文件操作语法。 open 函数调用中的“r”参数表示打开文件进行读取。 “for line in fin”语句每次会枚举文件中的一行,并将当前输入行赋予变量“line”。Python 的另一个简洁构造为元组特性。 元组使用左右括号表示,元组值由“,”字符分隔。 此处,我调用 split 方法并将生成的每个标记赋予变量 caseid、value1、value2、operation、decimals、action 和 expected,所有操作在一条语句中即可全部实现。 这真是好极了。

接下来,开始构建要发布到待测试的 Web 应用程序的数据:

...
expected = 'value="' + expected.Trim() + '" id="TextBox3"'
data = 'TextBox1=' + value1 + '&TextBox2=' + value2 + '&Operation=' +
 operation + '&DropDownList1=' + decimals + '&Button1=' + action

print 'Test case: ' + caseid
print 'Input    : ' + data
print 'Expected : ' + expected
...

我稍微修改了 expected 变量,使其与以下类似:

value="5.7900" id="TextBox3"

这样,在搜索 HTTP 响应时将会更具体,而不仅仅是搜索“5.7900”。我以“&”字符连接的名称值对方式将测试用例输入值与其各自控件相关联。 发布字符串的前两个名称值对只是将 TextBox1 和 TextBox2 设置为测试用例数据中的 value1 和 value2。 第三个名称值对(例如 Operation=RadioButton1)是我模拟用户选择 RadioButton 控件(在本例中即对应加法的控件)的方式。 您可能会误以为(就像我原来一样)设置单选按钮的方法是使用类似 RadioButton1=checked 的语法。 但是,RadioButton1 是 Operation 控件的值,而非控件本身。 第五个名称值对 Button1=clicked 容易让人产生误解。 我需要为 Button1 提供一个值以指示已单击该控件,不过由于任何值都适用,因此可使用 Button1=foo(甚至仅使用 Button1=),但我认为 Button1=clicked 更具描述性。 我向命令 Shell 回显从测试用例数据分析得到的值,并利用“+”字符串连接运算符。

接下来,处理 ViewState 和 EventValidation 值:

...
vs = getVS(url)
ev = getEV(url)
vs = HttpUtility.UrlEncode(vs)
ev = HttpUtility.UrlEncode(ev)
data = data + "&__VIEWSTATE=" + vs + "&__EVENTVALIDATION=" + ev
...

我调用了之前定义的 getVS 和 getEV 帮助程序函数。 ViewState 和 EventValidation 值为 Base64 编码的字符串。 Base64 编码使用 64 个字符:大写的 A-Z、小写的 a-z、数字 0-9、字符“+”和“/”。 字符“=”在 Base64 中用作填充。 由于 HTTP 请求流中禁用 Base64 中使用的某些字符,因此我使用 System.Web 命名空间的 HttpUtility.UrlEncode 方法,将带来麻烦的字符转换为一个以“%”字符开头的三字符序列。

例如,原始的“>”字符编码为 %3D,而空格编码为 %20。 当 Web 服务器收到的 HTTP 请求包含此类特殊的三字符序列时,服务器将把这些序列解码回原始输入。 在编码之后,将 ViewState 和 EventValidation 值附加到发布数据中。

接下来,处理发布数据以准备好发送 HTTP 请求:

...
buffer = Encoding.ASCII.GetBytes(data)
req = HttpWebRequest.Create(url)
req.Method = 'POST'
req.ContentType = 'application/x-www-form-urlencoded'
req.ContentLength = buffer.Length
...

我使用 System.Text 命名空间的 GetBytes 方法将发布数据转换为一个名为 buffer 的字节数组。 然后,使用显式 Create 方法创建新的 HttpWebRequest 对象。 我为 HttpWebRequest 对象的 Method、ContentType 和 ContentLength 属性提供了值。 您可以将 ContentType 的值视为发布 HTTP Web 请求所必需的魔幻字符串。

接下来,发送 HTTP 请求:

...
reqst = req.GetRequestStream()
reqst.Write(buffer, 0, buffer.Length)
reqst.Flush()
reqst.Close()
...

如果您不熟悉这种技术,可能会不太习惯用于发送请求的编程模式。 您不用使用某种显式 Send 方法,而是创建一个 Stream 对象,然后使用 Write 方法。

Write 方法需要字节数组、数组中开始写入的索引以及要写入的字节数。 通过使用 0 和 buffer.Length,可写入 buffer 中的所有字节。 Write 方法不会将 Post 实际发送到 Web 服务器,您必须调用 Flush 方法强制执行发送。 Close 方法会实际调用 Flush 方法,因此这种情况下不需要调用 Flush,但为清楚起见,我还是调用了 Flush。

在发送 HTTP 请求之后,提取相关联的响应:

...
res = req.GetResponse()
resst = res.GetResponseStream()
sr = StreamReader(resst)
html = sr.ReadToEnd()
sr.Close()
resst.Close()
...

GetResponse 方法会返回与 HttpWebRequest 对象相关联的 HttpWebResponse 对象。 该响应对象可用于创建 Stream,然后我将此 Stream 关联到 StreamReader 对象,并使用 ReadToEnd 方法将整个响应流作为单个字符串提取。 虽然底层的 .NET Framework 清理机制最终会关闭 StreamReader 和 Stream 对象,但我倾向于显式关闭这些对象。

接下来,检查 HTTP 响应是否为测试用例预期值:

...
if html.IndexOf(expected) >= 0:
  print 'Pass'
  numPass = numPass + 1
else:
  print '**FAIL**'
  numFail = numFail + 1
...

我使用 IndexOf 方法来搜索 HTTP 响应。 由于 IndexOf 会返回引用字符串中搜索字符串的开始位置,因此返回值 >= 0 表示搜索字符串位于引用字符串中。 请注意,与许多语言不同,Python 没有递增/递减运算符,如 ++numPass。

接下来完成脚本:

...
print '===============================\n'
    # end main processing loop
  fin.close()
  print '\nNumber pass = ' + str(numPass)
  print 'Number fail = ' + str(numFail)  
  print '\nEnd test run\n'
except:
  print 'Fatal: ', sys.exc_info()[0]
# end script

我在迭代测试用例数据文件的每一行的“for”循环结尾处插入了注释,以帮助确保缩进是正确的。一旦跳出循环,就可以关闭测试用例数据文件并输出通过计数器和失败计数器。请注意,由于 numPass 和 numFail 被推断为 int 类型,因此必须使用 Python str 函数将它们转换为“string”类型,以便进行连接。该工具完成时会处理 try 块中引发的任何异常,方式只是输出在内置 sys.exec_info 数组中存储的一般异常消息。

具有较短生命周期的快速自动化

我在此处介绍的示例应当为您提供了足够的信息,可以帮助您为自己的 Web 应用程序编写 IronPython 测试自动化程序。使用 IronPython 有多种替代方法。在我看来,IronPython 最适合小型测试自动化,在小型测试自动化中,您需要快速创建自动化,并且自动化具有较短的设计生命周期(几天或几周)。我的技术确实存在某些限制,特别是它无法轻松处理生成弹出对话框的 Web 应用程序。

与其他脚本编写语言相比,使用 IronPython 执行小型测试自动化的优点之一在于,可以使用交互式控制台发出命令来帮助编写脚本。另外,还提供了多种性能优异的编辑器,您甚至可以找到 IronPython SDK 将 Iron­Python 集成到 Visual Studio。就我个人而言,在准备编写 Web 应用程序测试自动化程序时,如果要开发相对简短的工具,我会考虑使用 IronPython 和 Notepad。不过,如果要编写代码超过三页的工具,我通常会使用 C# 和 Visual Studio。

James McCaffrey 博士 James McCaffrey 供职于 Volt Information Sciences, Inc.,在该公司他负责管理对华盛顿州雷蒙德市沃什湾 Microsoft 总部园区的软件工程师进行的技术培训。他曾参与过多项 Microsoft 产品的研发工作,其中包括 Internet Explorer 和 MSN Search。McCaffrey 是《.NET Test Automation Recipes:A Problem-Solution Approach》(Apress,2006 年)一书的作者。可通过 jammc@microsoft.com 与他联系。

*衷心感谢以下技术专家对本文进行了审阅:*Dave Fugate  Paul Newson