训练营第二天作业

训练营第二天作业

package cn.ianzhang.ceshiren;

import io.qameta.allure.Epic;
import io.qameta.allure.Feature;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;
import java.util.Objects;

/**
 * @Author: Ian
 * @Date: 2023/4/12 21:49
 * @Description:
 */

@Epic("CeShiRen")
@Feature("Day2")
@ExtendWith(MyTestExecutionExceptionHandler.class)
@ExtendWith(AllureWatcher.class)
@Slf4j
public class CeShiRenTest implements WebDriverGetter {
  private WebDriver driver;
  private WebDriverWait wait;

  @BeforeEach
  void setUp() {
    ChromeOptions options = new ChromeOptions();
    options.setHeadless(true);
    driver = new ChromeDriver(options);
    wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    log.info("最大化窗口");
    driver.manage().window().maximize();
    log.info("隐式等待并非银弹,但是题目要求使用,特此声明");
    driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));
    openCeShiRen();
  }

  @ParameterizedTest
  @Tag("冒烟用例")
  @Tag("正向用例")
  @ValueSource(strings = {"Selenium"})
  void doSimpleSearchTest(String keyword) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keyword);
    assertResultsDisplaying();
  }

  @ParameterizedTest
  @Tag("其他功能点")
  @Tag("高级搜索")
  @Tag("正向用例")
  @ValueSource(strings = {"Selenium"})
  void doAdvancedSearchTest(String keyword) {
    clickSearchButton();
    openAdvancedSearch();
    typeInKeyWordsIntoAdvancedSearchTerm(keyword);
    assertAdvancedResultsDisplaying();
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("异常场景")
  @Tag("搜索结果为空")
  @ValueSource(strings = {"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"
          , "QWERTYUIOP"})
  void doSimpleSearchWithNoResultsTest(String keyword) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keyword);
    assertNoSuchResult();
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("异常场景")
  @Tag("搜索内容特殊字符")
  @ValueSource(strings = {"!test@#$", "1+1=2>1"})
  void doSimpleSearchWithSpecialWordsTest(String keyword) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keyword);
    assertResultsDisplaying();
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("异常场景")
  @Tag("搜索内容为空")
  @ValueSource(strings = {" ", "  "})
  void doSimpleSearchWithBlankKeyWordsTest(String keyword) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keyword);
    assertKeyWordsIsTooShort();
  }

  /**
   * 某些时候可以触发414 error的弹窗,
   * 但是服务端响应414导致,并非源码JS拦截
   * 尚未明确具体条件
   * 可通过以下方法判断
   * assertDialogWithTextDisplaying("414 error");
   */
  @ParameterizedTest
  @Tag("业务场景")
  @Tag("异常场景")
  @Tag("搜索内容超长")
  @ValueSource(ints = {414, 1024, 2048})
  void doSimpleSearchWithLongKeyWordsTest(int size) {
    doSimpleSearchWithNoResultsTest(RandomStringUtils.randomAlphabetic(size));
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("补充场景")
  @Tag("搜索内容前后加空格,正常搜索到相关内容")
  @CsvSource(value = {"selenium,junit", "maven,test"})
  void doSimpleSearchWithMultipleKeyWordsTest(String keyword1, String keyword2) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(StringUtils.SPACE
            + keyword1
            + StringUtils.SPACE
            + keyword2
            + StringUtils.SPACE
    );
    assertSearchResultContains(keyword1, keyword2);
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("补充场景")
  @Tag("多次输入同意关键词,搜索结果一致")
  @ValueSource(strings = {"selenium", "junit"})
  void doSimpleSearchWithSameKeyWordTest(String keyword) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keyword);
    String result = fetchSearchResult();
    clearSearchTerm();
    typeInKeyWordsIntoSearchTerm(keyword);
    Assertions.assertThat(fetchSearchResult()).isEqualTo(result);
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("补充场景")
  @Tag("删除原搜索关键词,输入新关键词,可以正常搜索到相关内容")
  @CsvSource(value = {"selenium,junit", "gradle,jenkins"})
  void doSimpleSearchWithSameKeyWordsTest(String kw1, String kw2) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(kw1);
    assertSearchResultContains(kw1);
    clearSearchTerm();
    typeInKeyWordsIntoSearchTerm(kw2);
    assertSearchResultContains(kw2);
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("补充场景")
  @Tag("多个关键词,中间加空格,正常搜索到相关内容")
  @CsvSource(value = {"selenium,junit,maven", "gradle,test,jenkins"})
  void doSimpleSearchWithMultipleKeyWordsTest(String kw1, String kw2, String kw3) {
    String[] keywords = {kw1, kw2, kw3};
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(String.join(StringUtils.SPACE, keywords));
    assertSearchResultContains(keywords);
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("补充场景")
  @Tag("输入SQL语句,页面没有报错")
  @CsvSource(value = {"select * from tbl_sys_account", "select name from user"})
  void doSimpleSearchSqlWithoutAlertOrExceptionTest(String keywords) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keywords);
    assertNoDialogAlertDisplaying();
  }

  @ParameterizedTest
  @Tag("业务场景")
  @Tag("补充场景")
  @Tag("输入JS语句,页面没有报错")
  @CsvSource(value = {"alert(1)", "let a = 'test'"})
  void doSimpleSearchJsWithoutAlertOrExceptionTest(String keywords) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keywords);
    assertNoDialogAlertDisplaying();
  }


  @ParameterizedTest
  @Tag("如果代码执行出现异常,则截图当前执行的页面")
  @ValueSource(strings = {"selenium"})
  void doSimpleSearchExceptionTest(String keyword) {
    clickSearchButton();
    typeInKeyWordsIntoSearchTerm(keyword);
    assertDialogWithTextDisplaying("no error");
  }


  @AfterEach
  void tearDown() {
    if (Objects.nonNull(driver)) driver.close();
  }


  @Override
  public WebDriver getWebDriver() {
    return driver;
  }


  /**
   * 打开测试人网站
   */
  void openCeShiRen() {
    driver.get("https://ceshiren.com/");
  }

  void clickSearchButton() {
    wait.until(ExpectedConditions.elementToBeClickable(By.id("search-button")))
            .click();
  }

  void openAdvancedSearch() {
    wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//a[@title='打开高级搜索']")))
            .click();
  }

  void typeInKeyWordsIntoSearchTerm(String keyword) {
    wait.until(ExpectedConditions.elementToBeClickable(By.id("search-term")))
            .sendKeys(keyword + Keys.ENTER);
  }

  void typeInKeyWordsIntoAdvancedSearchTerm(String keyword) {
    wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//input[@placeholder='搜索']")))
            .clear();
    wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//input[@placeholder='搜索']")))
            .sendKeys(keyword);
    wait.until(ExpectedConditions.elementToBeClickable(By.xpath("//div[@class='search-bar']/button[@aria-label='搜索']")))
            .click();
  }

  void clearSearchTerm() {
    wait.until(ExpectedConditions.elementToBeClickable(By.id("search-term")))
            .clear();
  }

  void assertNoSuchResult() {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='results']")));
    Assertions.assertThat(driver
                    .findElement(By.xpath("//div[@class='results']"))
                    .getText())
            .isEqualTo("找不到结果。");
  }

  void assertKeyWordsIsTooShort() {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='results']")));
    Assertions.assertThat(driver
                    .findElement(By.xpath("//div[@class='results']"))
                    .getText())
            .isEqualTo("您的搜索词过短。");
  }

  void assertSearchResultContains(String... keywords) {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='search-result-topic']")));
    Assertions.assertThat(driver
                    .findElement(By.xpath("//div[@class='search-result-topic']"))
                    .getText())
            .containsAnyOf(keywords);
  }

  String fetchSearchResult() {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='search-result-topic']")));
    return driver
            .findElement(By.xpath("//div[@class='search-result-topic']"))
            .getText();
  }

  void assertResultsDisplaying() {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='search-result-topic']")));
    Assertions.assertThat(driver
                    .findElement(By.xpath("//div[@class='search-result-topic']"))
                    .getText())
            .isNotEmpty();
  }

  void assertAdvancedResultsDisplaying() {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath("//div[@class='search-advanced']/div[@class='search-results']")));
    Assertions.assertThat(driver
                    .findElement(By.xpath("//div[@class='search-advanced']/div[@class='search-results']"))
                    .getText())
            .isNotEmpty();
  }

  void assertDialogWithTextDisplaying(String text) {
    wait.until(ExpectedConditions.presenceOfElementLocated(By.id("dialog-holder")));
    Assertions.assertThat(driver
                    .findElement(By.xpath("//div[@id='dialog-holder']//div[@class='dialog-body']"))
                    .getText())
            .isEqualTo(text);
  }

  void assertNoDialogAlertDisplaying() {
    Assertions.assertThatThrownBy(() -> new WebDriverWait(driver, Duration.ofSeconds(3))
                    .until(ExpectedConditions.elementToBeClickable(By.id("dialog-holder"))))
            .isExactlyInstanceOf(TimeoutException.class);
    Assertions.assertThatNoException();
  }

}

感谢老师的批语,对于其中的并发问题有些疑惑,希望可以明示下具体场景。

  1. 当前的测试代码没有处理并行执行的情况,如果要实现并行执行,需要对WebDriver实例的管理进行优化

之前提交的示例代码是可以通过junit并发执行的,配置如下。其中数值没有设置过大是因为目标网站有相关验证,搜索过快会导致错误结果(附图),但是并没有出现因为并发执行导致程序判断有误的情况。

properties

junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=2
junit.jupiter.execution.parallel.config.fixed.max-pool-size=5

log

[ForkJoinPool-1-worker-4] INFO CeShiRenTest - 最大化窗口
[ForkJoinPool-1-worker-2] INFO CeShiRenTest - 最大化窗口
[ForkJoinPool-1-worker-3] INFO CeShiRenTest - 最大化窗口
[ForkJoinPool-1-worker-5] INFO CeShiRenTest - 最大化窗口
[ForkJoinPool-1-worker-1] INFO CeShiRenTest - 最大化窗口

当然,实际工作中,可以考虑池化既将WebDriver放入上下文中共享,不过本次提交并未提交完整项目,仅单一场景一个类足以体现Selenium相关的作业完成情况。
至于评语中提到的异常处理部分,实际通过实现TestExecutionExceptionHandler注入的Allure截图资源,不影响作业主体的Selenium部分,共享项目麻烦一些,故未额外上传其他文件内容,仅贴了测试主体文件内容。
老师的评语比较专业,都在点上,再次感谢。

很抱歉,一开始我将@BeforeEach以为成@BeforeAll导致了作业误判,@BeforeEach在实践中并不会触发线程安全的bug,当然对于并行执行的最佳实践推荐使用ThreadLocal。这一部分并不属于作业的要求,所以在评分过程没有因此扣分