Skip to content

9. 应用程序 [SimuPaie] – 版本 5 – ASP.NET / Web 服务


推荐阅读:参考文档 [2],《C# 2008 入门》第 10 章“Web 服务”


9.1. 应用程序的新架构

Pam 应用程序的分层架构目前如下:

我们将按以下方式对其进行升级:

而在之前的架构中,[web]、[metier]、[dao] 这三个层是在同一台虚拟机中运行的。NET,而在新架构中,[web]层将运行在与[metier]和[dao]层不同的虚拟机中。 特别是当 [web] 层位于 M1 机器上,而 [metier] 和 [dao] 层位于 M2 机器上的情况下。 这里采用的是客户端/服务器架构:

  • 服务器由 [metier] 和 [dao] 层组成。由于这是一个 Web 服务,因此需要 Web 服务器 2 号才能运行。
  • 客户端由 [web] 层构成。其运行需要第 1 号 Web 服务器。
  • 客户端和服务器通过 TCP/IP 网络,使用 HTTP / SOAP 协议进行通信。为此,需要在架构中添加两个新层:
    • [S]层,该层将作为Web服务。 Web服务接收来自远程客户端的请求,并利用[metier]和[dao]两层来处理这些请求。构建TCP/IP服务的方法多种多样。Web服务的优势体现在两方面:
      • 它采用 HTTP 协议,该协议能通过企业和政府机构的防火墙
      • 它采用标准的 HTTP / SOAP 子协议,该协议已被众多开发平台(如 .Net、Java、PHP、Flex 等)所实现 因此,Web服务可被.Net、Java、PHP、Flex等客户端“调用”(这是通常的术语)
    • [C]层将作为远程Web服务的客户端,其作用是与Web服务[S]进行通信。

这一新架构可轻松从先前架构演变而来:

  • [metier] 和 [dao] 层保持不变
  • [web]层略有调整,主要是为了引用诸如Employe、FeuilleSalaire等实体,这些实体已归属于客户端层[C]。 这些实体与 [metier] 或 [dao] 层中的实体类似,但它们属于不同的命名空间。
  • 服务器层 [S] 是一个类,它实现了 [metier] 层中的 IPamMetier 接口。该实现仅调用 [metier] 层中的相应方法。 由服务器层 [S] 实现的方法将“暴露”给远程客户端,以便客户端调用。
  • 客户端层 [C] 将由 Visual Studio 自动生成。

新架构的原则如下:

  • [web]层继续与[metier]层通信,就好像后者是本地层一样。为此,客户端层[C]实现了IPamMetier 接口,并向 [web] 层呈现为本地 [metier] 层。 除了前面提到的命名空间问题外,[web]层保持不变。这就是分层架构的优势。如果构建的是单层应用程序,就需要对其进行深度重构。
  • 客户端层 [C] 会以对 [web] 层透明的方式,将后者的请求转发给远程 Web 服务 [S]。它负责处理所有“网络通信”相关的事务。 它接收来自远程 Web 服务的响应,并对其进行格式化,以便以 [web] 层所期望的形式将其返回给该层。
  • 在服务器端,Web 服务 [S] 接收来自其远程客户端的命令。它对这些命令进行格式化,以便调用 [metier] 层中 IPamMetier 接口的方法。 当它收到来自 [metier] 层的响应后,会对其进行格式化,并通过网络将其传输给客户端 [C]。 [metier] 和 [dao] 层无需修改。

9.2. Web 服务的 Visual Web Developer 项目

我们使用 Visual Web Developer 构建一个新项目:

  • 在 [1] 中,我们选择 C# Web 项目
  • 在 [2] 中,我们选择“Web 服务应用程序 ASP.NET”
  • 在 [3] 中,为 Web 项目命名
  • 在 [4] 中,我们指定该项目的存储位置
  • 在 [1] 中,生成的项目。这是一个典型的 Web 项目,但有以下细节:
    • 我们指定该项目为“Web服务”类型。Web服务不会向客户端发送网页,而是发送XML格式的数据。因此,通常会生成的[Default.aspx]页面并未生成。
  • 在 [2] 中,生成了一个名为 [Service1.asmx] 的文件,其内容如下:

<%@ WebService Language="C#" CodeBehind="Service1.asmx.cs" Class="pam_v5_webservice.Service1" %>
  • (续)
    • - 标签 WebService 表示 [Service.asmx] 是一个 Web 服务
    • - 属性 CodeBehind 指明了该 Web 服务的源代码位置
    • - 属性 Class 指明了源代码中实现该 Web 服务的类名

默认生成的 Web 服务源代码 [Service.asmx.cs] 如下:


using System.Web.Services;

namespace pam_v5_webservice
{
  /// <summary>
  /// 服务1的简要说明
  /// </summary>
  [WebService(Namespace = "http://tempuri.org/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  [System.ComponentModel.ToolboxItem(false)]
  // 若要允许通过脚本使用 ASP.NET AJAX 调用此 Web 服务,请删除以下行中的注释标记。 
  // [System.Web.Script.Services.ScriptService]
  public class Service1 : System.Web.Services.WebService
  {

    [WebMethod]
    public string HelloWorld()
    {
      return "Hello World";
    }
  }
}
  • 第8行:释WebService,该注释使得第13行的类Service1将作为Web服务进行发布。 Web 服务属于某个命名空间,以避免全球范围内出现两个名称相同的 Web 服务。我们稍后将修改该命名空间。
  • 第 13 行:类 Service1 继承自 .NET 框架中的类 WebService
  • 第 16 行:注解 WebMethod 使得被注解的方法将对远程客户端公开,从而允许其调用该方法。
  • 第 17-20 行:方法 HelloWorld 是一个演示方法。我们稍后会将其删除。它使我们能够进行初步测试,并了解 Visual Studio 的工具以及有关 Web 服务的一些基本知识。
  • 在 [1] 中,我们调用 Web 服务 [Service.asmx]
  • VS Web Developer 已启动其内置 Web 服务器,并使其监听一个随机端口(此处为 1599)。随后向 Web 服务器请求了 URL [2],该 URL 指向 Web 服务的测试页面。
  • 在 [3] 中,有一个链接可用于查看 Web 服务的描述文件。 该文件因其后缀(.wsdl)而被称为 WSDL(WebService 描述语言),实际上是一个 XML 文件,用于描述 Web 服务所公开的方法。 正是通过该 WSDL 文件,客户端才能获知:
    • Web 服务的命名空间
    • Web 服务公开的方法列表
    • 每个方法所需的参数
    • 各方法返回的响应
  • 在 [4] 中, Web 服务公开的唯一方法。
  • 在 [5] 中,通过链接 [3] 获取的文件 WSDL 的内容。 需注意 URL 与 [6]。Web 服务用户必须了解这一点。
  • 在 [7] 中,通过链接 [4] 获取的页面可用于调用 Web 服务的 [HelloWorld] 方法
  • 在 [8] 中,得到的结果:一个 XML 响应。请注意该方法的 URL [9]。

通过研究前面的页面,可以理解如何调用Web服务的方法以及它返回何种类型的响应。这使得编写能够与Web服务进行交互的客户端成为可能。 目前大多数开发环境都支持自动生成此类客户端,从而免去了开发人员手动编写代码的麻烦。Visual Studio Express 便是其中之一。

在继续进行本项目之前,我们将更改生成类时默认使用的命名空间:

当我们选择项目属性(右键单击项目 / 属性)时,会看到项目名称为 [1],其默认命名空间为 l'assembly。

完成上述操作后,

  • 在 [Service1.asmx.cs] 中,我们将类的命名空间更改为:

using System.Web.Services;

namespace pam_v5
{
...
  public class Service1 : System.Web.Services.WebService
  {
...
  }
}
  • 在 [Service.asmx] 中,我们同样更改了类 [Service1] 所使用的命名空间(右键点击 / 查看标记):

<%@ WebService Language="C#" CodeBehind="Service1.asmx.cs" Class="pam_v5.Service1" %>

让我们回到应用程序的架构:

  • [S]层是Web服务。它仅负责向远程客户端暴露[metier]层的方法。我们正在构建的就是这一层。
  • [C]层是Web服务的客户端HTTP。IDE能够自动生成这一层。
  • 如果确保 [C] 层实现了远程 [metier] 层的接口。

下面我们可以看到,我们的 Web 服务将:

  • 暴露 [metier] 层的方法
  • 与该层进行交互,而该层又将与 [dao] 层进行交互。

因此,该项目必须使用 [metier] 和 [dao] 层中的 DLL。其演变过程如下:

  • 转换为 [1],向项目中添加引用
  • 在 [2] 中,从 [lib] 文件夹中选择常规的 DLL。需确保它们的“本地副本”属性均指向 True。 所选的 DLL 是那些在 NHibernate 支持下实现 [metier] 和 [dao] 层的文件。

“ASP.NET Web 服务”类型的 Web 应用程序可以像传统的“ASP.NET Web 站点”应用程序一样,拥有一个全局应用程序类“Global.asax”。我们已经看到了此类类的优势:

  • 它在应用程序启动时被实例化并驻留内存中
  • 因此可以存储所有客户端共享的只读数据。在我们的应用程序中,它将像之前那样存储简化的员工列表。这样可以避免在客户端请求时从数据库中检索该列表。
  • 在 [1] 中,右键单击项目
  • 在 [2] 中,选择 [Ajouter un nouvel élément] 选项
  • 在 [3] 中,选择 [Classe d'application globale]
  • 在 [4] 中,文件 [Global.asax] 已添加到项目中

文件 [Global.asax] 的内容如下:


<%@ Application Codebehind="Global.asax.cs" Inherits="pam_v5.Global" Language="C#" %>

文件 [Global.asax.cs] 的内容如下:


using System;

namespace pam_v5
{
  public class Global : System.Web.HttpApplication
  {

    protected void Application_Start(object sender, EventArgs e)
    {

    }
...
  }
}

Application_Start 方法中我们需要做什么?与之前的 Web 应用程序完全相同。让我们回到应用程序架构,并将 [Global] 类放置其中:

在上图中,

  • [Global] 类在 Web 服务启动时被实例化。只要 Web 服务处于活动状态,它就会驻留在内存中。
  • 类 [Global] 在其方法 [Application_Start] 中实例化了 [metier] 和 [dao] 层
  • 为提高性能,类 [Global] 将简化的员工列表存入一个内部字段。它将从该字段中返回员工列表。
  • Web 服务会在每次客户端请求时被实例化。处理完该请求后,它便会消失。 它不会直接调用 [metier] 层,而是调用 [Global] 类。该类将实现 [metier] 层的接口。

类 [Global] 与之前为应用程序构建的类类似:


using System;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;

namespace pam_v5
{
  public class Global : System.Web.HttpApplication
  {
    // --- 应用程序静态数据 ---
    public static Employe[] Employes;
    public static IPamMetier PamMetier = null;

    protected void Application_Start(object sender, EventArgs e)
    {
      // [metier] 层的实例化
      PamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
      // 获取员工简化列表 
      Employes = PamMetier.GetAllIdentitesEmployes();
    }

    // 员工简化列表
    static public Employe[] GetAllIdentitesEmployes()
    {
      return Employes;
    }

    // 某位员工的工资
    static public FeuilleSalaire GetSalaire(string SS, double heuresTravaillées, int joursTravailles)
    {
      return PamMetier.GetSalaire(SS, heuresTravaillées, joursTravailles);
    }
  }
}

类 [Global] 实现了接口 [IPamMetier],但声明中并未明确指出这一点:


  public class Global : System.Web.HttpApplication, IPamMetier

事实上,方法 GetAllIdentitesEmployes(第 24 行)和 GetSalaire(第 30 行)是静态的,而接口 IPamMetier 中的方法则不是。 因此,类 Global 无法实现接口 IPamMetier。 此外,无法将方法 GetAllIdentitesEmployesGetSalaire 声明为非静态方法。因为它们是通过类名访问的,而非通过该类的实例。

  • 第 15 行:方法 Application_Start 与之前版本中讨论的 [Global] 类中的方法类似。 它实例化了 [metier] 层(第 18 行),然后初始化(第 20 行)第 12 行中的员工数组。
  • 第 24 行:方法 GetAllIdentitesEmployes 仅返回第 12 行中的员工数组。这正是为何要在应用程序启动时就将其存储起来的原因。
  • 第30行:方法 GetSalaire 调用了 [metier] 层中同名的方法。

为了实例化 [metier] 层(第 18 行),[Global] 类使用了 Spring 框架。 该框架由文件 [Web.config] 进行配置,该文件与前一个项目中的配置文件完全相同:它配置了 Spring 和 NHibernate,以便实例化 Web 服务中的 [metier] 和 [dao] 层。

让我们回到客户端/服务器应用程序的架构:

在服务器端,现在只需编写 [S] Web 服务本身即可。如果我们回顾应用程序的架构:

我们可以看到,在服务器端,所有位于 [metier] 层之前的层都实现了该层的接口 IPamMetier。 虽然这不是强制要求的,但这似乎是一种合乎逻辑的做法。这种推理也可以应用于客户端,即 Web 服务 [S] 的客户端 [C]。 因此,所有将 [web] 层与 [metier] 层分隔开的层,都实现了 IPamMetier 接口。由此可以说,我们又回到了三层架构:

  • 展示层 [web] [1]
  • [metier] [2] 层
  • 数据访问层 [3]

Web服务[Service1.asmx.cs]的实现可能如下:


using System.Web.Services;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;

namespace pam_v5
{
  [WebService(Namespace = "http://st.istia.univ-angers.fr/")]
  [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
  [System.ComponentModel.ToolboxItem(false)]
  public class Service1 : System.Web.Services.WebService, IPamMetier
  {

    // 所有员工身份信息列表 
    [WebMethod]
    public Employe[] GetAllIdentitesEmployes()
    {
      return Global.GetAllIdentitesEmployes();
    }

    // ------- 工资计算 
    [WebMethod]
    public FeuilleSalaire GetSalaire(string ss, double heuresTravaillees, int joursTravailles)
    {
      return Global.GetSalaire(ss, heuresTravaillees, joursTravailles);
    }
  }
}
  • 第 8 行:该类被标注了 [WebService] 属性,并为 Web 服务的命名空间命名
  • 第 11 行:类 [Service1] 继承自类 [WebService],并实现了接口 [IPamMetier]
  • 第 15 行和第 22 行:该类的每个方法都标注了 [WebMethod] 属性,以便向远程客户端公开。默认情况下,Web 服务的所有公共方法都会被公开。 因此,第 15 行和第 22 行的属性在此处是可选的。为了实现 [IPamMetier] 接口,每个方法只需调用 [Global] 类中同名的方法即可。

现在我们可以运行该 Web 服务了:

  • 在 [1] 中,项目将重新生成
  • 在 [2] 中,选择 Web 服务 [Service1.asmx] 并在浏览器中显示 [3]
  • 在 [4] 中,显示的网页。该页面展示了 Web 服务的接口。
  • 在 [4] 中,我们点击链接 [GetAllIdentitesEmployes],并在 [5] 中获得该方法的测试页面。
  • 在 [6] 页面中,该方法的 URL
  • 在 [7] 中,[Appeler] 按钮用于测试该方法。该方法无需任何参数。
  • 在 [8] 中,即 Web 服务返回的 XML 结果。其中,仅 SSNomPrenom 具有实际意义,因为方法 [GetAllIdentitesEmployes] 仅请求这些属性。然而,该方法返回的是一个 Employe 对象数组。 从 [8] 中可以看出,数值属性 IdVersion 包含在返回的 XML 流中,但具有 null 值的属性则未包含: AdresseVilleCodePostalIndemnites

我们有一个正在运行的 Web 服务。 现在我们将为其编写一个 C# 客户端。为此,我们需要 Web 服务中 WSDL 文件中的 URI。我们可以在运行 [Service.asmx] 时最初显示的页面中获取它:

  • 在 [1] 中,Web 服务的 URI
  • 在 [2] 中,指向其文件 WSDL 的链接
  • 在 [3] 中,该链接的值

9.3. Web 服务的 NUnit 客户端的 C# 项目

我们为 Web 服务客户端创建一个 C# 项目(使用 Visual C# 而非 Visual Web Developer)。这将是一个测试客户端 NUnit。因此,该项目将采用“类库”类型。

  • 在 [1] 中,我们创建了一个“类库”类型的 C# 项目
  • 在 [2] 中,为项目命名
  • 在 [3] 中,打开该项目。我们删除 [Class1.cs]。
  • 在 [4] 中,新项目。
  • 在项目属性中的 [Application] [5] 选项卡中,我们设置了项目的命名空间。IDE 生成的每个类都将位于该命名空间中。

我们将新项目保存到合适的位置:

 

完成上述操作后,我们将生成远程 Web 服务的客户端。为了理解接下来的操作,我们需要回顾当前正在构建的客户端/服务器架构:

IDE 将基于 Web 服务 [S] 的文件 WSDL 中的 URI,生成客户端层 [C]。 需要提醒的是,该文件中的URI此前已被记录。我们按照 的步骤进行操作:

  • 转换为 [1],右键单击分支 References 并添加服务引用
  • 在 [2] 中,指定之前记录的 Web 服务文件 WSDL 中的 URL。如果该服务尚未启动,则需先启动它。
  • 在 [3] 中,通过其 WSDL 文件请求发现 Web 服务
  • 在 [4] 中,已发现的 Web 服务
  • 在 [5] 中,Web 服务公开的方法。
  • 在 [6] 中,指定要放置生成的客户端类和接口的命名空间。
  • 确认向导

  • 在 [1] 中,生成的客户端 。双击该文件即可查看其内容。
  • 在 [2] 中,对象浏览器中显示了命名空间 Client.WsPam 中的类和接口。这是生成的客户端的命名空间。
  • 在 [3] 中,是实现 Web 服务客户端的类。
  • 在 [4] 中,显示了由客户端 [Service1SoapClient] 实现的方法。其中包含远程 Web 服务 [5] 和 [6] 的两个方法。
  • 在 [2] 中,包含各层实体的图像:
    • [metier]:FeuilleSalaireElementsSalaire
    • [dao]:EmployeCotisationsIndemnites

接下来,需要记住这些远程实体的映像位于客户端,且位于 PamV5Client.WsPam 命名空间中。

让我们来考察其中一个实体所暴露的方法和属性:

  • 在 [1] 中,选择本地类 [Employe]
  • 在 [2] 中,我们可以看到远程实体 [Employe] 的属性,以及用于本地实体自身需求的私有字段。

让我们回到我们的 C# 应用程序。我们为其添加一个测试类 NUnit:

  • 在 [1] 中,已添加类 [NUnit]。 类 [NUnit] 将需要框架 NUnit,因此需要引用该框架中的 DLL。 此处假设框架 NUnit 已安装在该计算机上(http://nunit.org/)。
  • 在 [2] 中,向项目
  • 在“[3]”选项卡中,该选项卡汇总了本地保存的DLL文件,我们选择[4], DLL [nunit.framework] 最低版本为 2.4.6。

此外,我们将使用 Spring 来实例化 Web 服务 [S] 的本地客户端 [C]:

如果 DLL 此前已在本地机器上注册(http://www.springframework.net/download.html)。

我们的操作方式有所不同。我们使用先前项目中的 [lib] 文件夹(其中包含 Spring 所需的 DLL),并将 Spring 的引用添加到项目中:

让我们回顾一下正在构建的客户端架构:

上图中,我们可以看到测试客户端 [1] 与一个扩展的 [metier] [2] 层进行交互。该层提供了与远程 [metier] 层相同的方法。 因此,我们可以使用在 C# 项目 [pam-metier-dao-nhibernate] 中测试 [metier] 层时已用过的测试类:


using NUnit.Framework;
using Pam.Dao.Entites;
using Pam.Metier.Entites;
using Pam.Metier.Service;
using Spring.Context.Support;

namespace Pam.Metier.Tests {

    [TestFixture()]
    public class NunitTestPamMetier : AssertionHelper {

        // 待测试的 [metier] 层 
        private IPamMetier pamMetier;

        // 构造函数
        public NunitTestPamMetier() {
            // [dao] 层的实例化
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        }


        [Test]
        public void GetAllIdentitesEmployes() {
            // 员工人数核对 
            Expect(2, EqualTo(pamMetier.GetAllIdentitesEmployes().Length));
        }

        [Test]
        public void GetSalaire1() {
            // 计算工资单 
            FeuilleSalaire feuilleSalaire = pamMetier.GetSalaire("254104940426058", 150, 20);
            // 核查 
            Expect(368.77, EqualTo(feuilleSalaire.ElementsSalaire.SalaireNet).Within(1E-06));
            // 不存在的员工工资单 
            bool erreur = false;
            try {
                feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
            } catch (PamException) {
                erreur = true;
            }
            Expect(erreur, True);
        }

    }
}

需要进行一些修改:

  • 第 18 行,我们使用 Spring 框架实例化 [metier] 层。这两种情况下的类并不相同。 在此处,本地 [metier] 层是 [PamV5Client.WsPam.Service1SoapClient] 类的实例,该类由 IDE 生成。 因此,在 C# 项目的 [app.config] 文件中,Spring 的配置如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object id="pammetier" type="PamV5Client.WsPam.Service1SoapClient, pam-v5-client-csharp-webservice"/>
        </objects>
    </spring>


    <system.serviceModel>
...
  • 如上文第 16 行所示,对象 [pammetier] 是类 [PamV5Client.WsPam.Service1SoapClient] 的一个实例,该类位于程序集 (assembly) [pam-v5-client-csharp-webservice] 中。 要获取第一条信息,只需在对象资源管理器中返回 [Service1SoapClient] 类的定义(第 9.3 节):
  • 在 [2] 中,即本地 [metier] 层的实现类,以及在 [1] 中,其命名空间
  • 在 [3] 中,在项目属性中,程序集名称是配置 Spring 对象 [pammetier] 所需的第二项信息。

让我们回到 [NUnit.cs] 中本地层 [metier] 的实例化代码:


        // 待测试的 [metier] 层 
        private IPamMetier pamMetier;

        // 构造函数
        public NunitTestPamMetier() {
            // [dao] 层的实例化
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as IPamMetier;
        }

第 7 行,远程层 [metier] 的类型为 IPamMetier。而此处的 [metier] 层类型为 [Service1SoapClient]:


public class Service1SoapClient : System.ServiceModel.ClientBase<Service1Soap>

我们可以看到,尽管类 Service1SoapClient 暴露了同名方法,但它并未实现接口 IPamMetier。因此,我们需要按以下方式编写本地层 [metier] 的实例化:


        // 待测试的 [metier] 层 
        private Service1SoapClient pamMetier;

        // 构造函数
        public NunitTestPamMetier() {
            // [metier] 层的实例化
            pamMetier = ContextRegistry.GetContext().GetObject("pammetier") as Service1SoapClient;
}

另一项需要进行的修改:


        [Test]
        public void GetSalaire1() {
...
            try {
                feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
            } catch (PamException) {
                erreur = true;
            }
            Expect(erreur, True);
        }

上述代码在第 6 行使用了 PamException 类型,该类型在客户端并不存在。我们将用其父类 Exception 类型来替换它。


        [Test]
        public void GetSalaire1() {
...
            try {
                feuilleSalaire = pamMetier.GetSalaire("xx", 150, 20);
            } catch (Exception) {
                erreur = true;
            }
            Expect(erreur, True);
        }

最后,导入的命名空间也不再相同:


using System;
using PamV5Client.WsPam;
using NUnit.Framework;
using Spring.Context.Support;

完成上述步骤后,即可生成“类库”类型的项目。随后将创建以下 DLL:

  • 在 [1] 中,C# 项目的 [bin/Release] 文件夹
  • 在 [2] 中,该项目的 DLL。

随后,测试 NUnit 由框架 NUnit 执行(测试时,基础 MySQL 和 dbpam_nhibernate 必须处于活动状态):

  • 在 [3] 和 [4] 中,DLL [2] 被加载到测试应用程序 NUnit 中
  • 在 [5] 中,选定并执行了测试类 [6]
  • 在 [7] 中,测试结果显示为成功

现在我们有一个可运行的 Web 服务。