首页 > 解决方案 > 在 Selenium 测试设置中创建 cookie

问题描述

为一个相当冗长的问题道歉,但我认为我不能遗漏任何一个,并且仍然有一个最小的可重现示例。

问题

我的 Web 应用程序将用户名存储在 cookie 中。如果在用户访问应用程序时未找到 cookie,则应用程序会提示用户输入他们的姓名,然后将该姓名存储在 cookie 中。

如果在用户访问应用程序时发现了 cookie,则应用程序会通过存储在 cookie 中的名称向用户致意。

如果用户决定他们希望应用程序忘记他们,那么应用程序会删除 cookie,并且下次用户访问应用程序时,应用程序会提示用户输入他们的姓名。

当我手动测试应用程序时,一切似乎都运行良好。但是,当我使用 Selenium 运行用户已经拥有此类 cookie 的测试场景时,以验证应用程序是否按名称向他们打招呼时,应用程序的行为就像 cookie 不存在一样,而不是通过name 它提示他们输入他们的名字。

我相信我的测试场景的“安排”部分没有正确设置 cookie,但我不明白为什么。

工具和技术

该应用程序由一个 React.js 客户端和一个用 C# 编写的 ASP.net MVC 核心 Web API 服务器组成。

Selenium 测试项目使用 NUnit 作为测试运行程序,并使用 C# 编写。

Selenium 测试项目和 Web API 服务器都针对 .net 核心的 3.1 版本。

服务器

Web API 服务器项目有一个控制器,监听 /api/User 路由,具有 3 个操作方法:

用户控制器.cs

这是项目中唯一的控制器。

namespace CookieServer.Controllers
{
    using System;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;

    [Route("api/[controller]")]
    [ApiController]
    public class UserController : Controller
    {
        /// <summary>
        /// Gets the name of the current user from the UserName cookie, if it exists.
        /// </summary>
        /// <returns>200 with the user's name if the cookie exists, otherwise 404.</returns>
        [HttpGet]
        public IActionResult GetCurrent()
        {
            string userName;
            var cookieExists = this.HttpContext.Request.Cookies.TryGetValue("UserName", out userName);
            Console.WriteLine($"Cookie exists: {cookieExists}");
            if (cookieExists)
            {
                return this.Ok(new { UserName = userName }); // 200
            }
            else
            {
                return this.NotFound(); // 404
            }
        }

        /// <summary>
        /// Creates a cookie called "UserName" with the value set to the supplied userName parameter.
        /// If the user later visits the site from the same client machine and browser, we'll remember their name.
        /// </summary>
        /// <param name="userName">The username to store in the cookie.</param>
        /// <returns>200 if the username is OK, otherwise 400.</returns>
        [HttpPost]
        public IActionResult New(string userName)
        {
            if (string.IsNullOrWhiteSpace(userName))
            {
                return this.ValidationProblem("userName parameter must not be null or whitespace"); // 400
            }

            var options = new CookieOptions
            {
                // Set SameSite to Lax because the React.js client and this web API service are running
                // on different ports, and so count as different domains.
                SameSite = SameSiteMode.Lax,

                Secure = true, // Only send the cookie over HTTPS

                // Allow client-side JavaScript to access the cookie.
                // Normally we'd set this to true to prevent scripting attacks, but I want the client to
                // be able to log this cookie, otherwise the Selenium tests have no way of telling
                // whether or not the client has this cookie.
                HttpOnly = false,

                // The cookie will expire in about 8 thousand years.
                // This isn't quite the same as never expiring, but I'm pretty sure .net core 3.1 will be
                // out of support by then ;-)
                Expires = DateTime.MaxValue,
            };
            this.HttpContext.Response.Cookies.Append("UserName", userName, options);
            return this.Ok(); // 200
        }

        /// <summary>
        /// Deletes the UserName cookie, and so effectively forgets the user.
        /// </summary>
        /// <returns>200.</returns>
        [HttpDelete]
        public IActionResult Forget()
        {
            this.HttpContext.Response.Cookies.Delete("UserName");
            return this.Ok(); // 200
        }
    }
}

启动.cs

namespace CookieServer
{
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Net.Http.Headers;

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        /// <summary>
        /// This method gets called by the runtime. Use this method to add services to the container.
        /// </summary>
        /// <param name="services">Service collection</param>
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors(options =>
                options.AddPolicy("Dev", builder =>
                    builder.WithMethods("GET", "POST", "PUT", "DELETE")
                        .WithHeaders(
                            HeaderNames.Accept,
                            HeaderNames.ContentType,
                            HeaderNames.Authorization)
                        .AllowCredentials()
                        .SetIsOriginAllowed(origin =>
                        {
                            if (string.IsNullOrWhiteSpace(origin)) { return false; }

                            // Remove the next line in production
                            if (origin.ToLower().StartsWith("http://localhost")) { return true; }
                            return false;
                        })));

            services.AddControllers();
        }

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        /// <param name="app">Application builder.</param>
        /// <param name="env">Web host environment.</param>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseCors("Dev");

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

客户端

充当客户端的 React.js 应用程序是使用 create-react-app 创建的,它为我配置了 Babel,以便我可以使用 ES6 语言功能,例如类和 fetch API,而不必担心用户的浏览器是否支持它们.

我的组件.jsx

这是我添加到由 create-react-app 创建的应用程序中的唯一组件。它向服务器发出 GET 请求以查明 UserName cookie 是否存在,并根据响应提示用户输入他们的姓名(并向服务器发出 POST 请求以创建 UserName cookie),或者通过名称向用户打招呼并给他们一个“忘记我”按钮(它向服务器发出 DELETE 请求以删除 UserName cookie)。

import React from 'react';

export default class MyComponent extends React.Component {
    /**
     * Initializes a new instance of the MyComponent class.
     * @param {object} props values passed to the component as JSX attributes.
     */
    constructor(props) {
        super(props);
        this.state = {
            checkingUserName: false,
            userName: '',
            apiError: '',
            enteredUserName: '',
        };

        // The port number in this URL is taken from the "sslPort" key
        // of the launchSettings.json file in the web API project.
        this.apiUrl = 'https://localhost:44358/api/User';
    }

    /**
     * Called by the React runtime immediately after the component is mounted.
     * In this component we want to call the web API to see whether it knows
     * who the user is.
     */
    componentDidMount = () => {
        this.checkWhoIAm();
    }

    /**
     * Called by the React runtime when the component's state changes.
     * @returns {string} JSX markup representing the component.
     */
    render = () => {
        const nameTextBox = (
            <input 
                type="text" 
                id="nameTextBox" 
                placeholder="e.g. John Doe" 
                onChange={(e) => this.setState({enteredUserName: e.target.value})}
            />
        );
        const rememberMeButton = (
            <button 
                type="button" 
                id="rememberMeButton" 
                onClick={this.rememberMe} 
            >
                Remember me
            </button>
        );
        const forgetMeButton = (
            <button 
                type="button" 
                id="forgetMeButton" 
                onClick={this.forgetMe} 
            >
                Forget me
            </button>
        );
        const apiError = this.state.apiError ? <div>{this.state.apiError}</div> : '';

        let jsx;
        if (this.state.checkingUserName) {
            jsx = <div>Checking who you are...</div>;
        } else if (this.state.userName) {
            jsx = (
                <div>
                    {apiError}
                    <div id="welcome">Welcome, {this.state.userName}</div>
                    {forgetMeButton}
                </div>
            );
        } else {
            jsx = (
            <div>
                {apiError}
                <div>Please enter your name:</div>
                {nameTextBox}
                {rememberMeButton}
            </div>
            );
        }

        return jsx;
    }

    /**
     * Makes a GET request to the web API to see whether there's 
     * already a UserName cookie, and if so stores the user's 
     * name in the component state.
     */
    checkWhoIAm = async () => {
        const options = {
            headers: {},
            method: 'GET',
            credentials: 'include',
            mode: 'cors',
        };

        this.setState({
            checkingUserName: true,
            userName: '',
            apiError: '',
        });

        try {
            console.log("GET");
            const response = await fetch(this.apiUrl, options);
            if (response.status === 200) {
                console.log(`Cookies: ${document.cookie}`);
                this.setState({
                    checkingUserName: false,
                    userName: (await response.json()).userName,
                    apiError: '',
                })                
            } else {
                console.log(`Cookies: ${document.cookie}`);
                this.setState({
                    checkingUserName: false,
                    userName: '',
                });
            }
        } catch (exception) {
            this.setState({
                checkingUserName: false,
                userName: '',
                apiError: exception.message,
            });
        }
    }

    /**
     * Makes a POST request to the web API to store the name 
     * the user entered in a UserName cookie.
     */
    rememberMe = async () => {
        const options = {
            headers: {'Content-Type': 'application/json'},
            body: {},
            method: 'POST',
            credentials: 'include',
            mode: 'cors',
        };

        try {
            console.log("POST");
            const url = `${this.apiUrl}?userName=${this.state.enteredUserName}`;
            const response = await fetch(url, options);
            if (response.status === 200) {
                console.log(`Cookies: ${document.cookie}`);
                this.setState({
                    userName: this.state.enteredUserName,
                    apiError: '',
                });
            } else {
                this.setState({apiError: JSON.stringify(await response.json())});
            }
        } catch (exception) {
            this.setState({apiError: exception.message});
        }
    }

    /**
     * Makes a DELETE request to the web API to delete the UserName cookie.
     * The user has a right to be forgotten!
     */
    forgetMe = async () => {
        const options = {
            method: 'DELETE',
            credentials: 'include',
            mode: 'cors',
        }

        try {
            console.log("DELETE");
            const response = await fetch(this.apiUrl, options);
            if (response.status === 200) {
                console.log(`Cookies: ${document.cookie}`);
                this.setState({
                    userName: '',
                    apiError: '',
                });
            } else {
                this.setState({apiError: JSON.stringify(await response.json())});
            }
        } catch (exception) {
            this.setState({apiError: exception.message});
        }
    }
}

应用程序.js

该文件由 create-react-app 创建,默认呈现 React 徽标。我将其更改为渲染 MyComponent 。

import './App.css';
import MyComponent from './MyComponent';

function App() {
  return (
    <MyComponent />
  );
}

export default App;

硒测试

该测试尝试创建一个 UserName cookie,导航到应用程序的主页,并断言 ID 为“welcome”的元素包含 cookie 中的用户名。然后它捕获写入浏览器控制台的内容,以便我可以看到 UI 向服务器发出的请求,以及 UI 认为它具有的 cookie。

namespace SeleniumCookiesUITest
{
    using System;
    using System.Linq;
    using System.Net.Http;
    using System.Threading.Tasks;
    using NUnit.Framework;
    using OpenQA.Selenium;
    using OpenQA.Selenium.Chrome;
    using OpenQA.Selenium.Support.UI;

    [TestFixture]
    public class SeleniumCookieTests
    {
        /// <summary>
        /// URL of the server-side API service.
        /// </summary>
        private readonly string _apiUrl = "https://localhost:44358/api/User/";

        /// <summary>
        /// URL of the React.js client.
        /// </summary>
        private readonly string _uiUrl = "http://localhost:3000";

        [Test]
        public async Task GivenIAmAnExistingUser_WhenILoadThePage_ThenIWillBeGreetedByName()
        {
            // Arrange
            var options = new ChromeOptions();
            options.SetLoggingPreference(LogType.Browser, LogLevel.All);
            var driver = new ChromeDriver(options);
            try
            {
                var userName = Guid.NewGuid().ToString();

                // Uncomment one of the following lines and comment out the others
                SetCookieUsingDriver(driver, userName);
                //await SetCookieUsingApi(userName);
                //SetCookieUsingUI(driver, userName);

                // Act
                driver.Navigate().GoToUrl(_uiUrl);
                var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));

                // Assert - fails with timeout - 'Unable to locate element: {"method":"css selector","selector":"#welcome"}'
                var welcome = wait.Until(e => e.FindElement(By.Id("welcome")));
                StringAssert.Contains(userName, welcome.Text);
            }
            finally
            {
                var logs = driver.Manage().Logs.GetLog(LogType.Browser).ToList();
                if (logs.Count > 0)
                {
                    Console.WriteLine($"The following was written to the Browser log...");
                }

                foreach (var log in logs)
                {
                    Console.WriteLine($"{log.Timestamp} {log.Level} {log.Message}");
                }

                driver.Quit();
            }
        }

        /// <summary>
        /// Creates a UserName cookie using the method shown at
        /// <see href="https://www.selenium.dev/documentation/en/support_packages/working_with_cookies/"/>.
        /// </summary>
        /// <param name="driver">IWebDriver instance.</param>
        /// <param name="userName">User name to store in the cookie.</param>
        private void SetCookieUsingDriver(IWebDriver driver, string userName)
        {
            driver.Navigate().GoToUrl(_uiUrl);
            driver.Manage().Cookies.AddCookie(new Cookie("UserName", userName));

            // Check the cookie has been created
            Assert.AreEqual(userName, driver.Manage().Cookies.GetCookieNamed("UserName").Value);
        }

        /// <summary>
        /// Creates a UserName cookie by making a POST request to the API.
        /// </summary>
        /// <param name="userName">User name to store in the cookie.</param>
        /// <returns>A Task representing the asynchronous operation.</returns>
        /// <remarks>
        /// This method feels wrong because the cookie doesn't seem to be associated with the driver, although
        /// <see href="https://www.selenium.dev/documentation/en/guidelines_and_recommendations/generating_application_state/"/>
        /// could be interpreted as suggesting this method.
        /// </remarks>
        private async Task SetCookieUsingApi(string userName)
        {
            var client = new HttpClient();

            // This POST request will create the cookie
            var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}?userName={userName}");
            var response = await client.SendAsync(httpRequest);
            response.EnsureSuccessStatusCode();

            // This GET request returns the username set in the POST request to prove that the cookie has been created
            httpRequest = new HttpRequestMessage(HttpMethod.Get, _apiUrl);
            response = await client.SendAsync(httpRequest);
            response.EnsureSuccessStatusCode();
            var responseContent = await response.Content.ReadAsStringAsync();
            StringAssert.Contains(userName, responseContent);
        }

        private void SetCookieUsingUI(IWebDriver driver, string userName)
        {
            driver.Navigate().GoToUrl(_uiUrl);
            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
            var textBox = wait.Until(e => e.FindElement(By.Id("nameTextBox")));
            var button = wait.Until(e => e.FindElement(By.Id("rememberMeButton")));
            textBox.Clear();
            textBox.SendKeys(userName);
            button.Click();
        }
    }
}

我尝试了 3 种不同的方法来创建 cookie。无论我使用哪一个,测试都会失败,因为当我期望显示欢迎消息时,会显示输入用户名的提示。

SetCookieUsingDriver

我相信这使用了Selenium's working with cookies page中描述的方法,但是应用程序没有找到 cookie,而是提示用户输入他们的姓名,因此没有显示 ID 为“welcome”的元素。使用此方法时测试的控制台输出:

The following was written to the Browser log...
31/07/2021 12:29:04 Info http://localhost:3000/static/js/vendors~main.chunk.js 37052:14 "[HMR] Waiting for update signal from WDS..."
31/07/2021 12:29:04 Info http://localhost:3000/static/js/main.chunk.js 385:16 "GET"
31/07/2021 12:29:04 Severe https://localhost:44358/api/User - Failed to load resource: the server responded with a status of 404 ()
31/07/2021 12:29:04 Info http://localhost:3000/static/js/main.chunk.js 396:18 "Cookies: "
31/07/2021 12:29:04 Info http://localhost:3000/static/js/vendors~main.chunk.js 37052:14 "[HMR] Waiting for update signal from WDS..."
31/07/2021 12:29:04 Info http://localhost:3000/static/js/main.chunk.js 385:16 "GET"
31/07/2021 12:29:04 Severe https://localhost:44358/api/User - Failed to load resource: the server responded with a status of 404 ()
31/07/2021 12:29:04 Info http://localhost:3000/static/js/main.chunk.js 396:18 "Cookies: UserName=aa6d2d23-0534-4b03-9681-bf6a091f8cec"

第一个 GET 请求返回 404 状态,我期待这是因为该请求只是在创建 cookie 之前让浏览器进入正确的域。在第二个 GET 请求中,UI 似乎认为它有一个 UserName cookie,但它没有被发送到服务器,或者服务器在请求中没有找到它。

SetCookieUsingApi

此方法不是使用 Selenium 创建 cookie,而是在启动应用程序之前向服务器发出 POST 请求以创建 cookie。回想起来,这种方法感觉不对,因为在创建的任何 cookie 与随后打开的浏览器窗口之间似乎没有任何关联,但我想我会尝试一下。

The following was written to the Browser log...
31/07/2021 12:42:31 Info http://localhost:3000/static/js/vendors~main.chunk.js 37052:14 "[HMR] Waiting for update signal from WDS..."
31/07/2021 12:42:31 Info http://localhost:3000/static/js/main.chunk.js 385:16 "GET"
31/07/2021 12:42:31 Severe https://localhost:44358/api/User - Failed to load resource: the server responded with a status of 404 ()
31/07/2021 12:42:31 Info http://localhost:3000/static/js/main.chunk.js 396:18 "Cookies: "

SetCookieUsingUI

此方法使用 UI 创建 cookie,它导航到应用程序的主页,在文本框中输入名称,单击“记住我”按钮,然后再次导航到主页以测试输入的名称现在是否显示。这违反了Selenium 关于生成应用程序状态的指导方针,其中状态

Selenium 不应该用于准备测试用例。一个测试用例的所有重复动作和准备,都应该通过其他方法来完成。

然而,这种方法也与应用程序的使用方式最相似,所以最令人惊讶的是,这种方法也失败了。

The following was written to the Browser log...
31/07/2021 12:50:30 Info http://localhost:3000/static/js/vendors~main.chunk.js 37052:14 "[HMR] Waiting for update signal from WDS..."
31/07/2021 12:50:30 Info http://localhost:3000/static/js/main.chunk.js 385:16 "GET"
31/07/2021 12:50:31 Severe https://localhost:44358/api/User - Failed to load resource: the server responded with a status of 404 ()
31/07/2021 12:50:31 Info http://localhost:3000/static/js/main.chunk.js 396:18 "Cookies: "
31/07/2021 12:50:31 Info http://localhost:3000/static/js/main.chunk.js 423:16 "POST"
31/07/2021 12:50:31 Info http://localhost:3000/static/js/main.chunk.js 428:18 "Cookies: "
31/07/2021 12:50:31 Info http://localhost:3000/static/js/vendors~main.chunk.js 37052:14 "[HMR] Waiting for update signal from WDS..."
31/07/2021 12:50:31 Info http://localhost:3000/static/js/main.chunk.js 385:16 "GET"
31/07/2021 12:50:31 Severe https://localhost:44358/api/User - Failed to load resource: the server responded with a status of 404 ()
31/07/2021 12:50:31 Info http://localhost:3000/static/js/main.chunk.js 396:18 "Cookies: "

这一次,UI 似乎在任何时候都不会认为它有一个 UserName cookie。

问题

我在这里做错了什么?手动测试时应用程序的行为完全符合预期这一事实让我相信我的 UI 和服务器代码是正确的,因此问题一定出在测试创建 cookie 的方式上。我在 React 和 ASP.net MVC 核心方面相当有经验,但几个月前才开始使用 Selenium,这一事实为这种观点提供了支持。

然而,这也是我第一次尝试使用 cookie,所以我仍然对服务器端代码中的 cookie 处理不符合标准的可能性持开放态度。

标签: c#reactjsseleniumselenium-webdrivercookies

解决方案


推荐阅读