写测试用例的时候经常发现,所写的功能需要Http上下文的支持(session,cookie)这类的.

  以下介绍2种应用场景.

用于控制器内Requet获取参数

  控制器内的Requet其实是控制器内的属性.那么在mock的时候把那些上下文附加到Controller里的控制器上下文(ControllerContext )里,request自然就有东西了.

  1. public Controller()
  2.  
  3. {
  4. /// <summary>
  5. /// 获取或设置控制器上下文。
  6. /// </summary>
  7. ///
  8. /// <returns>
  9. /// 控制器上下文。
  10. /// </returns>
  11. public ControllerContext ControllerContext { get; set; }
  12.  
  13. /// <summary>
  14. /// 为当前 HTTP 请求获取 HttpRequestBase 对象。
  15. /// </summary>
  16. ///
  17. /// <returns>
  18. /// 请求对象。
  19. /// </returns>
  20. public HttpRequestBase Request
  21. {
  22. get
  23. {
  24. if (this.HttpContext != null)
  25. return this.HttpContext.Request;
  26. return (HttpRequestBase) null;
  27. }
  28. }
  29. }

  

为此,为了单独的Mock这些http上下文中的一些元素,我们需要6个类

Mock类

  1. //http://stephenwalther.com/archive/2008/07/01/asp-net-mvc-tip-12-faking-the-controller-context
  2. public class FakeControllerContext : ControllerContext
  3. {
  4. //public FakeControllerContext(ControllerBase controller)
  5. // : this(controller, null, null, null, null, null, null)
  6. //{
  7. //}
  8.  
  9. /// <summary>
  10. /// MockCookie
  11. /// </summary>
  12. /// <param name="controller"></param>
  13. /// <param name="cookies"></param>
  14. public FakeControllerContext(ControllerBase controller, HttpCookieCollection cookies)
  15. : this(controller, null, null, null, null, cookies, null)
  16. {
  17. }
  18.  
  19. /// <summary>
  20. /// MockSession
  21. /// </summary>
  22. /// <param name="controller"></param>
  23. /// <param name="sessionItems"></param>
  24. public FakeControllerContext(ControllerBase controller, SessionStateItemCollection sessionItems)
  25. : this(controller, null, null, null, null, null, sessionItems)
  26. {
  27. }
  28.  
  29. /// <summary>
  30. /// MockForm
  31. /// </summary>
  32. /// <param name="controller"></param>
  33. /// <param name="formParams"></param>
  34. public FakeControllerContext(ControllerBase controller, NameValueCollection formParams)
  35. : this(controller, null, null, formParams, null, null, null)
  36. {
  37. }
  38.  
  39. /// <summary>
  40. /// MockForm+QueryString
  41. /// </summary>
  42. /// <param name="controller"></param>
  43. /// <param name="formParams"></param>
  44. /// <param name="queryStringParams"></param>
  45. public FakeControllerContext(ControllerBase controller, NameValueCollection formParams, NameValueCollection queryStringParams)
  46. : this(controller, null, null, formParams, queryStringParams, null, null)
  47. {
  48. }
  49.  
  50. public FakeControllerContext(ControllerBase controller, string userName)
  51. : this(controller, userName, null, null, null, null, null)
  52. {
  53. }
  54.  
  55. public FakeControllerContext(ControllerBase controller, string userName, string[] roles)
  56. : this(controller, userName, roles, null, null, null, null)
  57. {
  58. }
  59.  
  60. /// <summary>
  61. /// Mock Session+Cookie+Form+QuertyString+IIdentity
  62. /// </summary>
  63. /// <param name="controller">控制器名</param>
  64. /// <param name="userName"></param>
  65. /// <param name="roles"></param>
  66. /// <param name="formParams">Form</param>
  67. /// <param name="queryStringParams">QueryString</param>
  68. /// <param name="cookies">Cookie</param>
  69. /// <param name="sessionItems">Session</param>
  70. public FakeControllerContext
  71. (
  72. ControllerBase controller,
  73. string userName,
  74. string[] roles,
  75. NameValueCollection formParams,
  76. NameValueCollection queryStringParams,
  77. HttpCookieCollection cookies,
  78. SessionStateItemCollection sessionItems
  79. )
  80. : base(new FakeHttpContext(
  81. new FakePrincipal(new FakeIdentity(userName), roles),
  82. formParams,
  83. queryStringParams,
  84. cookies, sessionItems), new RouteData(), controller)
  85. { }
  86.  
  87. /// <summary>
  88. ///
  89. /// </summary>
  90. /// <param name="controller"></param>
  91. /// <param name="formParams"></param>
  92. /// <param name="queryStringParams"></param>
  93. /// <param name="cookies"></param>
  94. /// <param name="sessionItems"></param>
  95. /// <param name="userName"></param>
  96. /// <param name="roles"></param>
  97. public FakeControllerContext
  98. (
  99. ControllerBase controller,
  100. NameValueCollection formParams,
  101. NameValueCollection queryStringParams,
  102. HttpCookieCollection cookies,
  103. SessionStateItemCollection sessionItems,
  104. string userName = null,
  105. string[] roles = null
  106. )
  107. : base(new FakeHttpContext(
  108. new FakePrincipal(new FakeIdentity(userName), roles),
  109. formParams,
  110. queryStringParams,
  111. cookies, sessionItems), new RouteData(), controller)
  112. { }
  113. }
  114.  
  115. public class FakeHttpContext : HttpContextBase
  116. {
  117. private readonly FakePrincipal _principal;
  118. private readonly NameValueCollection _formParams;
  119. private readonly NameValueCollection _queryStringParams;
  120. private readonly HttpCookieCollection _cookies;
  121. private readonly SessionStateItemCollection _sessionItems;
  122.  
  123. public FakeHttpContext(FakePrincipal principal, NameValueCollection formParams, NameValueCollection queryStringParams, HttpCookieCollection cookies, SessionStateItemCollection sessionItems)
  124. {
  125. _principal = principal;
  126. _formParams = formParams;
  127. _queryStringParams = queryStringParams;
  128. _cookies = cookies;
  129. _sessionItems = sessionItems;
  130. }
  131.  
  132. public override HttpRequestBase Request { get { return new FakeHttpRequest(_formParams, _queryStringParams, _cookies); } }
  133.  
  134. public override IPrincipal User { get { return _principal; } set { throw new System.NotImplementedException(); } }
  135. public override HttpSessionStateBase Session { get { return new FakeHttpSessionState(_sessionItems); } }
  136. }
  137.  
  138. public class FakeHttpRequest : HttpRequestBase
  139. {
  140. private readonly NameValueCollection _formParams;
  141. private readonly NameValueCollection _queryStringParams;
  142. private readonly HttpCookieCollection _cookies;
  143.  
  144. public FakeHttpRequest(NameValueCollection formParams, NameValueCollection queryStringParams, HttpCookieCollection cookies)
  145. {
  146. _formParams = formParams;
  147. _queryStringParams = queryStringParams;
  148. _cookies = cookies;
  149. }
  150.  
  151. public override NameValueCollection Form
  152. {
  153. get
  154. {
  155. return _formParams;
  156. }
  157. }
  158.  
  159. public override NameValueCollection QueryString
  160. {
  161. get
  162. {
  163. return _queryStringParams;
  164. }
  165. }
  166.  
  167. public override HttpCookieCollection Cookies
  168. {
  169. get
  170. {
  171. return _cookies;
  172. }
  173. }
  174.  
  175. }
  176.  
  177. public class FakeHttpSessionState : HttpSessionStateBase
  178. {
  179. private readonly SessionStateItemCollection _sessionItems;
  180.  
  181. public FakeHttpSessionState(SessionStateItemCollection sessionItems)
  182. {
  183. _sessionItems = sessionItems;
  184. }
  185.  
  186. public override void Add(string name, object value)
  187. {
  188. _sessionItems[name] = value;
  189. }
  190.  
  191. public override int Count
  192. {
  193. get
  194. {
  195. return _sessionItems.Count;
  196. }
  197. }
  198.  
  199. public override IEnumerator GetEnumerator()
  200. {
  201. return _sessionItems.GetEnumerator();
  202. }
  203.  
  204. public override NameObjectCollectionBase.KeysCollection Keys
  205. {
  206. get
  207. {
  208. return _sessionItems.Keys;
  209. }
  210. }
  211.  
  212. public override object this[string name]
  213. {
  214. get
  215. {
  216. return _sessionItems[name];
  217. }
  218. set
  219. {
  220. _sessionItems[name] = value;
  221. }
  222. }
  223.  
  224. public override object this[int index]
  225. {
  226. get
  227. {
  228. return _sessionItems[index];
  229. }
  230. set
  231. {
  232. _sessionItems[index] = value;
  233. }
  234. }
  235.  
  236. public override void Remove(string name)
  237. {
  238. _sessionItems.Remove(name);
  239. }
  240. }
  241.  
  242. public class FakeIdentity : IIdentity
  243. {
  244. private readonly string _name;
  245.  
  246. public FakeIdentity(string userName) { _name = userName; }
  247.  
  248. public string AuthenticationType { get { throw new System.NotImplementedException(); } }
  249.  
  250. public bool IsAuthenticated { get { return !String.IsNullOrEmpty(_name); } }
  251.  
  252. public string Name { get { return _name; } }
  253.  
  254. }
  255.  
  256. public class FakePrincipal : IPrincipal
  257. {
  258. private readonly IIdentity _identity;
  259. private readonly string[] _roles;
  260.  
  261. public FakePrincipal(IIdentity identity, string[] roles)
  262. {
  263. _identity = identity;
  264. _roles = roles;
  265. }
  266.  
  267. public IIdentity Identity { get { return _identity; } }
  268.  
  269. public bool IsInRole(string role)
  270. {
  271. if (_roles == null)
  272. return false;
  273. return _roles.Contains(role);
  274. }
  275. }

  在原示例里面那个外国佬还mock了其他东西( IPrincipal User).但对于我来说没这方面需求.

  然后我们测试一下.

测试控制器

  1. public class TestController : Controller
  2. {
  3. #region 请求模拟输出
  4. public ActionResult TestSession()
  5. {
  6. return Content(Session["hehe"].ToString());
  7. }
  8.  
  9. public ActionResult TestCookie()
  10. {
  11. var cookie = Request.Cookies["hehe"];
  12. if (cookie == null)
  13. return new EmptyResult();
  14. return Content(cookie.Values["c1"]);
  15. }
  16.  
  17. #endregion
  18.  
  19. #region 请求测试
  20. public ActionResult TestForm()
  21. {
  22. string fuckyou = Request.Form["fuckyou"];
  23. if (fuckyou == null)
  24. return new EmptyResult();
  25. return Content(fuckyou);
  26. }
  27.  
  28. public ActionResult TestFormAndQueryString()
  29. {
  30. string form = Request.Form["fuckyou"];
  31. string querty = Request.QueryString["fuckyou2"];
  32. return Content(form + "," + querty);
  33. }
  34.  
  35. public ActionResult TestMuilt()
  36. {
  37. var session = Session["hehe"].ToString();
  38. var cookie = Request.Cookies["hehe"].Values["c1"];
  39. string fuckyou = Request.Form["fuckyou"];
  40. string querty = Request.QueryString["fuckyou2"];
  41. return Content(string.Format("{1} {0} {2} {0}{3} {0} {4} {0}", Environment.NewLine, session, cookie, fuckyou, querty));
  42. }
  43. #endregion
  44.  
  45. }

  测试类

  1. [TestClass]
  2. public class MockRequestTest
  3. {
  4. private readonly IUserCenterService _IUserCenterService;
  5. public MockRequestTest()
  6. {
  7. EngineContext.Initialize(false);
  8. _IUserCenterService = EngineContext.Current.Resolve<IUserCenterService>();
  9. }
  10.  
  11. [Test]
  12. [TestMethod]
  13. public void MockSession()
  14. {
  15. //_IUserCenterService = EngineContext.Current.Resolve<IUserCenterService>();
  16. var controller = new TestController();
  17. var sessionItems = new SessionStateItemCollection();
  18. sessionItems["hehe"] = 23;
  19. controller.ControllerContext = new FakeControllerContext(controller, sessionItems);
  20. var result = controller.TestSession() as ContentResult;
  21. Assert.AreEqual(result.Content, "23");
  22. }
  23.  
  24. [TestMethod]
  25. public void MockCookie()
  26. {
  27. var controller = new TestController();
  28. var mockCookie = new HttpCookie("hehe");
  29. mockCookie["c1"] = "nima1";
  30. mockCookie["c2"] = "nima2";
  31. var requestCookie = new HttpCookieCollection() { { mockCookie } };
  32. controller.ControllerContext = new FakeControllerContext(controller, requestCookie);
  33. var result = controller.TestCookie() as ContentResult;
  34. Console.WriteLine(HttpContext.Current == null);
  35. Assert.AreEqual("nima1", result.Content);
  36. }
  37.  
  38. /// <summary>
  39. /// MockForm
  40. /// </summary>
  41. [TestMethod]
  42. public void MockForm()
  43. {
  44. var controller = new TestController();
  45. NameValueCollection form = new FormCollection()
  46. {
  47. {"fuckyou","1"},
  48. {"fuckyou","2"},
  49. };
  50. controller.ControllerContext = new FakeControllerContext(controller, form);
  51. var result = controller.TestForm() as ContentResult;
  52. Debug.Assert(false, result.Content);
  53. Assert.IsNotNull(result.Content);
  54. }
  55.  
  56. /// <summary>
  57. /// MockForm
  58. /// </summary>
  59. [TestMethod]
  60. public void MockFormAndQueryString()
  61. {
  62. var controller = new TestController();
  63. NameValueCollection form = new FormCollection()
  64. {
  65. {"fuckyou","1"},
  66. {"fuckyou2","2"},
  67. };
  68. controller.ControllerContext = new FakeControllerContext(controller, form, form);
  69. var result = controller.TestFormAndQueryString() as ContentResult;
  70. //Debug.Assert(false, result.Content);
  71. Assert.AreEqual("1,2", result.Content);
  72. }
  73.  
  74. /// <summary>
  75. /// Mock Session+Cookie+Form+QuertyString
  76. /// </summary>
  77. [TestMethod]
  78. public void MockMuilt()
  79. {
  80. var controller = new TestController();
  81. var sessionItems = new SessionStateItemCollection();
  82. sessionItems["hehe"] = 23;
  83.  
  84. var mockCookie = new HttpCookie("hehe");
  85. mockCookie["c1"] = "nima1";
  86. mockCookie["c2"] = "nima2";
  87. var requestCookie = new HttpCookieCollection() { { mockCookie } };
  88.  
  89. NameValueCollection form = new FormCollection()
  90. {
  91. {"fuckyou","1"},
  92. {"fuckyou2","2"},
  93. };
  94.  
  95. controller.ControllerContext = new FakeControllerContext(controller, form, form, requestCookie, sessionItems);
  96. var result = controller.TestMuilt() as ContentResult;
  97. Debug.Assert(
  98. false,
  99. result.Content,
  100. string.Format("正确的结果顺序应该是{0};{1};{2};{3};", sessionItems[0], mockCookie["c1"], form["fuckyou"], form["fuckyou2"])
  101. );
  102. }
  103. }

  在上面这个MS测试用例里,我分别测试了

  • Mock session
  • Mock cookie
  • Mock表单
  • Mock 表单+querystring
  • Mock session+cookie+表单+querystring

  都是通过的.

但是这样有个问题.

问题就是:然而这并没有什么卵用.

mock HttpContext.Current

  实际开发的时候.控制器基本打酱油,别的层面需要获取上下文是从HttpContext.Current.Request中获取.如果在刚才的测试用例.控制器输出的是HttpContext.Current.Request.这玩意无疑是null的.因为我们只是把上下文赋值到控制器里的http上下文里面,和HttpContext.Current.Reques是不同的一个概念.

  所以呢,我们需要mock 和HttpContext.Current.Request.

  session的话,比较容易,那就是

  1. SessionStateUtility.AddHttpSessionStateToContext

  cookie的话比较麻烦.HttpRequest.Cookies是一个只读属性,就算用反射赋值也会失败.这里我比较取巧,只用了cookie集合的第一个.有多个的话,可能得把方法改得更恶心一点吧.

代码

  1. public static class WebExtension
  2. {
  3. /// <summary>
  4. /// 伪造session
  5. /// </summary>
  6. /// <param name="url"></param>
  7. /// <param name="sesion"></param>
  8. /// <param name="queryString"></param>
  9. /// <param name="requesttype"></param>
  10. public static void FakeHttpContext(this string url, SessionStateItemCollection sesion, string queryString = null, string requesttype = null, HttpCookieCollection cookie = null)
  11. {
  12. var stringWriter = new StringWriter();
  13. var httpResponce = new HttpResponse(stringWriter);
  14. HttpRequest request;
  15. if (cookie == null)
  16. {
  17. request = new HttpRequest(string.Empty, url, queryString ?? string.Empty)
  18. {
  19. RequestType = requesttype ?? "GET",
  20. };
  21. }
  22. else
  23. {
  24. request = new HttpRequest(string.Empty, url, queryString ?? string.Empty)
  25. {
  26. RequestType = requesttype ?? "GET",
  27. Cookies = { cookie[0] },
  28. };
  29. }
  30. var httpContext = new HttpContext(request, httpResponce);
  31. if (sesion != null)
  32. {
  33. SessionStateUtility.AddHttpSessionStateToContext(httpContext,
  34. new HttpSessionStateContainer(SessionNameStorage.Suser,
  35. sesion,
  36. new HttpStaticObjectsCollection(),
  37. 20000,
  38. true,
  39. HttpCookieMode.AutoDetect,
  40. SessionStateMode.InProc,
  41. false
  42. ));
  43. }
  44. if (cookie != null)
  45. {
  46. //无法对只读属性赋值,会导致异常
  47. //Type ret = typeof(HttpRequest);
  48. //PropertyInfo pr = ret.GetProperty("Cookies");
  49. //pr.SetValue(request, cookie, null); //赋值属性
  50.  
  51. }
  52.  
  53. //var sessionContainer = new HttpSessionStateContainer(
  54. // "id",
  55. // new SessionStateItemCollection(),
  56. // new HttpStaticObjectsCollection(),
  57. // 10,
  58. // true,
  59. // HttpCookieMode.AutoDetect,
  60. // SessionStateMode.InProc,
  61. // false);
  62.  
  63. //httpContext.Items["AspSession"] =
  64. // typeof(HttpSessionState).GetConstructor(
  65. // BindingFlags.NonPublic | BindingFlags.Instance,
  66. // null,
  67. // CallingConventions.Standard,
  68. // new[] { typeof(HttpSessionStateContainer) },
  69. // null).Invoke(new object[] { sessionContainer });
  70.  
  71. HttpContext.Current = httpContext;
  72. }
  73.  
  74. }

  

相应控制器以及测试用例

  1. public ActionResult TestHttpCurrent()
  2. {
  3. var a = System.Web.HttpContext.Current;
  4. if (a != null)
  5. {
  6. return Content(a.Request.Cookies.Get("hehe").Value);
  7. }
  8. return Content("");
  9. }
  10.  
  11. [TestMethod]
  12. public void httpCurrent()
  13. {
  14. var controller = new TestController();
  15. var mockCookie = new HttpCookie("hehe");
  16. mockCookie["c1"] = "nima1";
  17. mockCookie["c2"] = "nima2";
  18. var requestCookie = new HttpCookieCollection() { { mockCookie } };
  19. string.Format("{0}/test/TestHttpCurrent", TestHelper.WebRootUrl).FakeHttpContext(sesion: null, cookie: requestCookie);
  20. var result = controller.TestHttpCurrent() as ContentResult;
  21. Console.WriteLine(result.Content);
  22.  
  23. }

  session就不测了,我平时测试的时候试了无数次都是有的.

备注:

mock cookie那里,如果有更好的实现方式,请告诉我.

标题是故意为之的,代表了我对ASB.NET 的嘲讽.

参考链接:

ASP.NET MVC Tip #12 – Faking the Controller Context

  1.  

ASP.NET MVC, HttpContext.Current is null while mocking a request

Mock session,cookie,querystring in ASB.NET MVC的更多相关文章

  1. Asp.net MVC使用Model Binding解除Session, Cookie等依赖

    上篇文章"Asp.net MVC使用Filter解除Session, Cookie等依赖"介绍了如何使用Filter来解除对于Session, Cookie的依赖.其实这个也可以通 ...

  2. Asp.net MVC使用Filter解除Session, Cookie等依赖

    本文,介绍了Filter在MVC请求的生命周期中的作用和角色,以及Filter的一些常用应用场景. 同时针对MVC中的对于Session,Cookie等的依赖,如何使用Filter解依赖. 如果大家有 ...

  3. [转]Asp.net MVC使用Filter解除Session, Cookie等依赖

    本文转自:http://www.cnblogs.com/JustRun1983/p/3279139.html 本文,介绍了Filter在MVC请求的生命周期中的作用和角色,以及Filter的一些常用应 ...

  4. web也是区分前端与后端的,session\cookie辨析

    <1>Ajax交互方式 Ext.Ajax.request( { //被用来向服务器发起请求默认的url url : "", //请求时发送后台的参数,既可以是Json对 ...

  5. Asp.net 服务器Application,Session,Cookie,ViewState和Cache区别

    2.8 Context 的使用Context 对象包含与当前页面相关的信息,提供对整个上下文的访问,包括请求.响应.以及上文中的Session 和Application 等信息.可以使用此对象在网页之 ...

  6. session & cookie(li)

    Session & Cookie 一.定义 Session,用户在浏览某个网站时,从进入网站到浏览器关闭所经过的这段时间,也就是用户浏览这个网站所花费的时间.Cookie,由服务器端生成,发送 ...

  7. 浅析session&cookie

    session&cookie没有出现的黑暗时代 大家都知道,HTTP协议是一种无状态的协议,本次请求下一次请求没有任何的关联,所有没有办法直接用http协议来记住用户的信息,试想一向,每一次点 ...

  8. http之Session&Cookie

    百度了一波session与Cookie,我发现这东西远比我想象中更复杂(可能是因为我不明白底层的运行原理).网上也是一堆的关于Session与Cookie区别/联系的文章,然而,我看完了还是一脸懵逼的 ...

  9. [转载]JavaEE学习篇之——Session&&Cookie

    原文链接: http://blog.csdn.net/jiangwei0910410003/article/details/23337043 今天继续来看看JavaWeb的相关知识,这篇文章主要来讲一 ...

随机推荐

  1. WinForm企业级框架实战项目演练

    一.课程介绍 我们都知道在软件架构方式分为:C/S和B/S两类.这里阿笨不谈论两种软件架构的优劣之分,因为它们各有千秋,用于不同场合.一位伟大的讲师曾经说过一句话:事物存在即合理!录制这堂课程的目的就 ...

  2. 【文文殿下】[AH2017/HNOI2017]礼物

    题解 二项式展开,然后暴力FFT就好了.会发现有一个卷积与c无关,我们找一个最小的项就行了. Tips:记得要倍长其中一个数组,防止FFT出锅 代码如下: #include<bits/stdc+ ...

  3. spring mvc 使用kaptcha配置生成验证码实例

    SpringMVC整合kaptcha(验证码功能) 一.依赖 <dependency> <groupId>com.github.penggle</groupId> ...

  4. 【rocketMQ】1、搭建MQ服务器,生产一个订单与消费一个订单

    1. 先解压 2. maven编译安装.(注意虚拟机采用nat网络模式,需要联网) mvn -Prelease-all -DskipTests clean install -U 启动nameser节点 ...

  5. the fist blood of java-eclipse 哈哈哈哈 封装的运用

    class Student {    private int id;    public String name;    public String sex;    private int score ...

  6. Java实现二叉树先序,中序,后序,层次遍历

    一.以下是我要解析的一个二叉树的模型形状.本文实现了以下方式的遍历: 1.用递归的方法实现了前序.中序.后序的遍历: 2.利用队列的方法实现层次遍历: 3.用堆栈的方法实现前序.中序.后序的遍历. . ...

  7. 【Canal源码分析】整体架构

    本文详解canal的整体架构. 一.整体架构 说明: server代表一个canal运行实例,对应于一个jvm instance对应于一个数据队列 (1个server对应1..n个instance) ...

  8. ActiveMQ配置高可用性的方式

    当一个应用被部署于生产环境,灾备计划是非常重要的,以便从网络故障,硬件故障,软件故障或者电源故障中恢复.通过合理的配置ActiveMQ,可以解决上诉问题.最典型的配置方法是运行多个Broker,一旦某 ...

  9. 全网最详细的用pip安装****模块报错:Could not find a version that satisfies the requirement ****(from version:) No matching distribution found for ****的解决办法(图文详解)

    不多说,直接上干货! 问题详情 这个问题,很普遍.如我这里想实现,Windows下Anaconda2 / Anaconda3里正确下载安装用来向微信好友发送消息的itchat库. 见,我撰写的 全网最 ...

  10. php -- 表单多选

    ----- 011-form.html ----- <!DOCTYPE html> <html> <head> <meta http-equiv=" ...