diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml new file mode 100644 index 0000000..69175b4 --- /dev/null +++ b/.github/workflows/on-pr.yml @@ -0,0 +1,28 @@ +name: On Pull Request + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 24 + uses: actions/setup-java@v4 + with: + distribution: oracle + java-version: 24 + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build + run: mvn -q compile diff --git a/.github/workflows/test-execution.yml b/.github/workflows/test-execution.yml index 380c8a6..338cdd1 100644 --- a/.github/workflows/test-execution.yml +++ b/.github/workflows/test-execution.yml @@ -2,30 +2,32 @@ name: Build and Test on: push: branches: - - master + - main pull_request: branches: - - master + - main jobs: - build: - runs-on: ubuntu-latest - services: - chrome: - image: selenium/standalone-chrome - options: --health-cmd '/opt/bin/check-grid.sh' + local-test: + runs-on: macos-latest + steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - name: Set up JDK 24 + uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 24 + distribution: oracle + - name: Cache Maven packages - uses: actions/cache@v1 + uses: actions/cache@v4 with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven - run: mvn -DskipTests -B package --file pom.xml - - name: Run the tests - run: mvn test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome -Dtestng.dtd.http=true \ No newline at end of file + run: mvn -q -DskipTests package + + - name: Run local tests + run: mvn -q test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome diff --git a/pipeline_as_code/.gitlab-ci.yml b/.sdlc/.gitlab-ci.yml similarity index 59% rename from pipeline_as_code/.gitlab-ci.yml rename to .sdlc/.gitlab-ci.yml index 7cada51..bf76065 100644 --- a/pipeline_as_code/.gitlab-ci.yml +++ b/.sdlc/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: maven:3.6.3-jdk-11 +image: maven:3.8.4-openjdk-17 stages: - build @@ -17,4 +17,4 @@ build: test: stage: test script: - - mvn test -Pweb-execution -Dsuite=local -Dtarget=local -Dtestng.dtd.http=true \ No newline at end of file + - mvn test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome diff --git a/pipeline_as_code/Jenkinsfile b/.sdlc/Jenkinsfile similarity index 89% rename from pipeline_as_code/Jenkinsfile rename to .sdlc/Jenkinsfile index 5261f50..21899b8 100644 --- a/pipeline_as_code/Jenkinsfile +++ b/.sdlc/Jenkinsfile @@ -12,7 +12,7 @@ node { stage('Test Execution') { try { - sh "'${mvnHome}/bin/mvn' test -Pweb-execution -Dtarget=local -Dsuite=local -Dtestng.dtd.http=true" + sh "'${mvnHome}/bin/mvn' test -Pweb-execution -Dsuite=local -Dtarget=local -Dheadless=true -Dbrowser=chrome } catch (Exception e) { currentBuild.result = 'FAILURE' } finally { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6234999..21a0ef8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Do not forget to add a _label_ on the issue or feature. Excellent! Thank you to help me out! You're going to need a few things first: -* JDK 11+ +* JDK 17+ * [Configure your IDE](https://projectlombok.org/setup/overview) in order to support Lombok. ## Send a pull request diff --git a/README.MD b/README.MD index 3627c7e..d90fecc 100644 --- a/README.MD +++ b/README.MD @@ -1,38 +1,45 @@ # Lean Test Automation Architecture using Java and Selenium WebDriver + [![Actions Status](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/workflows/Build%20and%20Test/badge.svg)](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/actions) -## Important information -```shell -This current version has excluded the guava library from WebDriverManager and Allure Environment Writter -due to a conflict with the guava version on Selenium 4 -``` +**This project delivers to you a complete lean test architecture for your web tests using the best frameworks and +practices.** + +It has a complete solution to run tests in different ways: + +* local testing using the browser on your local machine +* parallel (or single) testing using Selenium Docker +* local testing using TestContainers +* Distributed execution using Selenium Grid + +## Examples + +### Local testing execution example -**This project delivers to you a complete lean test architecture for your web tests using the best frameworks and practices.** +![Local testing execution example](assets/example_filed_test_with_report.gif) -Local testing execution example -![Local testing execution example](example_filed_test_with_report.gif) +### Parallel testing execution example with Selenium Grid -Parallel testing execution example with Docker Selenium -![Parallel testing execution example with Docker Selenium](selenium-grid-execution.gif) +![Parallel testing execution example with Selenium Grid](assets/selenium-grid-execution.gif) ## Languages and Frameworks -This project using the following languages and frameworks: +This project uses the following languages and frameworks: -* [Java 11](https://openjdk.java.net/projects/jdk/11/) as the programming language +* [Java 23](https://openjdk.java.net/projects/jdk/23/) as the programming language * [TestNG](https://testng.org/doc/) as the UnitTest framework to support the test creation * [Selenium WebDriver](https://www.selenium.dev/) as the web browser automation framework using the Java binding * [AssertJ](https://joel-costigliola.github.io/assertj/) as the fluent assertion library * [Allure Report](https://docs.qameta.io/allure/) as the testing report strategy -* [JavaFaker](https://github.com/DiUS/java-faker) as the faker data generation strategy +* [DataFaker](https://www.datafaker.net/) as the faker data generation strategy * [Log4J2](https://logging.apache.org/log4j/2.x/) as the logging management strategy -* [WebDriverManager](https://github.com/bonigarcia/webdrivermanager) as the Selenium binaries management * [Owner](http://owner.aeonbits.org/) to minimize the code to handle the properties file - +* [TestContainers](https://java.testcontainers.org/modules/webdriver_containers/) Webdriver Containers ## Test architecture -We know that any automation project starting with a good test architecture. +We know that any automation project starts with a good test architecture. + This project can be your initial test architecture for a faster start. You will see the following items in this architecture: @@ -46,15 +53,18 @@ You will see the following items in this architecture: * [Test Data Factory](#test-data-factory) * [Profiles executors on pom.xml](#profiles-executors-on-pomxml) * [Pipeline as a code](#pipeline-as-a-code) +* [Test environment abstraction](#execution-with-docker-selenium-distributed) Do you have any other items to add to this test architecture? Please do a pull request or open an issue to discuss. ### Page Objects pattern -I will not explain the Page Object pattern because you can find a lot of good explanations and examples on the internet. + +I will not explain the Page Object pattern because you can find a lot of good explanations and examples on the internet. Instead, I will explain what exactly about page objects I'm using in this project. #### AbstractPageObject -This class has a protected constructor to remove the necessity to init the elements using the Page Factory. + +This class has a protected constructor to remove the necessity to init the elements using the Page Factory. Also, it sets the timeout from the `timeout` property value located on `general.properties` file. All the Page Object classes should extend the `AbstractPageObject`. @@ -65,87 +75,152 @@ It also tries to remove the `driver` object from the Page Object class as much a > There's a `NavigationPage` on the `common` package inside the Page Objects. > Notice that all the pages extend this one instead of the `AbstractPageObject`. I implemented this way: > * because the previous and next buttons are fixed on the page (there's no refresh on the page) -> * to avoid create or pass the new reference to the `NavigationPage` when we need to hit previous or next buttons +> * to avoid creating or passing the new reference to the `NavigationPage` when we need to hit previous or next buttons As much as possible avoid this strategy to not get an `ElementNotFoundException` or `StaleElementReferenceException`. Use this approach if you know that the page does not refresh. ### Execution types -There are two execution types: **local** and **remote**. -The `TargetFactory` class will resolve the target execution based on the `target` property value located on `general.properties` file. -Its usage is placed on the `BaseWeb` class before each test execution. +There are different execution types: + +- `local` +- `local-suite` +- `selenium-grid` +- `testcontainers` + +The `TargetFactory` class will resolve the target execution based on the `target` property value located +on `general.properties` file. Its usage is placed on the `BaseWeb` class before each test execution. #### Local execution -This execution type uses [WebDriverManager](https://github.com/bonigarcia/webdrivermanager) class to instantiate the web browsers. -When the `target` is `local` the `createDriver()` method is used from the `BrowserFactory` class to return the browser instance. -The browser used in the test is placed on the `browser` property in the `local.properties` file. +##### Local machine + +**This approach is automatically used when you run the test class in your IDE.** + +When the `target` is `local` the `createLocalDriver()` method is used from the `BrowserFactory` class to return the +browser instance. + +The browser used in the test is placed on the `browser` property in the `general.properties` file. + +##### Local Suite + +It's the same as the Local Execution, where the difference is that the browser is taken from the TestNG suite file +instead of the `general.properties` +file, enabling you to run multi-browser test approach locally. + +##### Testcontainers + +This execution type uses the [WebDriver Containers](https://www.testcontainers.org/modules/webdriver_containers/) in +Testcontainers to run the tests in your machine, but using the Selenium docker images for Chrome or Firefox. + +When the `target` is `testcontainers` the `TargetFactory` uses the `createTestContainersInstance()` method to initialize +the container based on the browser set in the `browser` property. Currently, Testcontainers only supports Chrome and +Firefox. + +Example + +```shell +mvn test -Pweb-execution -Dtarget=testcontainers -Dbrowser=chrome +``` #### Remote execution -This execution is based on any Selenium Grid approach to execute the tests in remote machines (local or remote/cloud grid). -When the `target` is `remote` the `getOptions` method is used from the `BrowserFactory` to return the browser option + +##### Selenium Grid + +The Selenium Grid approach executes the tests in remote machines (local or remote/cloud grid). +When the `target` is `selenium-grid` the `getOptions` method is used from the `BrowserFactory` to return the browser +option class as the remote execution needs the browser capability. -The `DriverFactory` class has an internal method `createRemoteInstance` to return a `RemoteWebDriver` instance based on + +The `DriverFactory` class has an internal method `createRemoteInstance` to return a `RemoteWebDriver` instance based on the browser capability. You must pay attention to the two required information regarding the remote execution: the `grid.url` and `grid.port` property values on the `grid.properties` file. You must update these values before the start. -If you are using the `docker-compose.yml` file to start the Docker Selenium grid, the values on the `grid.properties` file should work. +If you are using the `docker-compose.yml` file to start the Docker Selenium grid, the values on the `grid.properties` +file should work. -Please take a look at the [Parallel Execution](#parallel-execution) section. +You can take a look at the [Execution with Docker Selenium Distributed](#execution-with-docker-selenium-distributed) +to run the parallel tests using this example. #### BrowserFactory class + This Factory class is a Java enum that has all implemented browsers to use during the test execution. -Each browser is an enum, and each enum implements two methods: -* `createDriver()`: creates the browser instance for the local execution. The browser driver is automatically managed by the WebDriverManager library -* `getOptions()`: creates a new browser Options setting some specific configurations. It's used for the remote executions +Each browser is an `enum`, and each enum implements four methods: + +* `createLocalDriver()`: creates the browser instance for the local execution. The browser driver is automatically + managed by the WebDriverManager library +* `createDriver()`: creates the browser instance for the remote execution +* `getOptions()`: creates a new browser `Options` setting some specific configurations, and it's used for the remote + executions using the Selenium Grid +* `createTestContainerDriver()` : Creates selenium grid lightweight test container in Standalone mode with + Chrome/Firefox/Edge browser support. -You can see that the `createDriver()` method used the `getOptions()` to use specific configuration, as starting the browser maximized and others. -The `getOptions()` is also used for the remote execution as it is a subclass of the `AbstractDriverOptions` and can be -automatically accepted as either a `Capabilities` or `MutableCapabilities` class, which is required by the `RemoteWebDriver` class. +You can see that the `createLocalDriver()` method use the `getOptions()` to get specific browser configurations, as +starting the browser maximized and others. + +The `getOptions()` is also used for the remote execution as it is a subclass of the `AbstractDriverOptions` and can be +automatically accepted as either a `Capabilities` or `MutableCapabilities` class, which is required by +the `RemoteWebDriver` class. #### DriverManager class -The class [DriverManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/driver/DriverManager.java) + +The +class [DriverManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/driver/DriverManager.java) create a `ThreadLocal` for the WebDriver instance, to make sure there's no conflict when we run it in parallel. ### BaseTest -This testing pattern was implemented on the [BaseWeb](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/test/java/com/eliasnogueira/BaseWeb.java) + +This testing pattern was implemented on +the [BaseWeb](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/test/java/com/eliasnogueira/BaseWeb.java) class to automatically run the pre (setup) and post (teardown) conditions. -The pre-condition uses `@BeforeMethod` from TestNG creates the browser instance based on the values passed either local or remote execution. +The pre-condition uses `@BeforeMethod` from TestNG creates the browser instance based on the values passed either local +or remote execution. The post-condition uses `@AfterMethod` to close the browser instance. Both have the `alwaysRun` parameter as `true` to force the run on a pipeline. -Pay attention that it was designed to open a browser instance to each `@Test` located on the test class. +Pay attention that it was designed to open a browser instance to each `@Test` located in the test class. -This class also the `TestListener` that is a custom TestNG listener, and will be described in the next section. +This class also has the `TestListener` annotation which is a custom TestNG listener, and will be described in the next +section. ### TestListener -The `TestListener` is a class that implements [ITestListener](https://testng.org/doc/documentation-main.html#logging-listeners). +The `TestListener` is a class that +implements [ITestListener](https://testng.org/doc/documentation-main.html#logging-listeners). The following method is used to help logging errors and attach additional information to the test report: -* `onTestStart`: add the browser information into the test report + +* `onTestStart`: add the browser information to the test report * `onTestFailure`: log the exceptions and add a screenshot to the test report -* `onTestSkipped`: add the skipped test on the log +* `onTestSkipped`: add the skipped test to the log ### Logging + All the log is done by the Log4J using the `@Log4j2` annotation. The `log4j2.properties` has two strategies: console and file. -A file with all the log information will be automatically created on the user folder with `test_automation.log` filename. +A file with all the log information will be automatically created on the user folder with `test_automation.log` +filename. If you want to change it, update the `appender.file.fileName` property value. -The `log.error` is used to log all the exceptions this architecture might throw. Use `log.info` or `log.debug` to log -important information, like the users, automatically generated by the factory [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) +The `log.error` is used to log all the exceptions this architecture might throw. Use `log.info` or `log.debug` to log +important information, like the users, automatically generated by the +factory [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) ### Parallel execution -The parallel test execution is based on the [parallel tests](https://testng.org/doc/documentation-main.html#parallel-tests) -feature on TestNG. This is used by `parallel.xml` test suite file which has the `parallel="tests"` attribute and value, + +The parallel test execution is based on +the [parallel tests](https://testng.org/doc/documentation-main.html#parallel-tests) +feature on TestNG. This is used by `selenium-grid.xml` test suite file which has the `parallel="tests"` attribute and +value, whereas `test` item inside the test suite will execute in parallel. The browser in use for each `test` should be defined by a parameter, like: + ```xml + ``` @@ -154,55 +229,80 @@ You can define any parallel strategy. It can be an excellent combination together with the grid strategy. #### Execution with Docker Selenium Distributed + This project has the `docker-compose.yml` file to run the tests in a parallel way using Docker Selenium. -To be able to run it in parallel the file has the [Dynamic Grid Implementation](https://github.com/SeleniumHQ/docker-selenium#dynamic-grid-) that will start the container on demand. +To be able to run it in parallel the file has +the [Dynamic Grid Implementation](https://github.com/SeleniumHQ/docker-selenium#dynamic-grid-) that will start the +container on demand. This means that Docker Selenium will start a container test for a targeting browser. -Please not you need the following before run it in parallel: +Please note that you need to do the following actions before running it in parallel: + * Docker installed -* Pull the images for Chrome and Firefox - * `docker pull selenium-standalog-chrome` - * `docker pull selenium-standalog-firefox` -* Pay attention at the `grid/config.toml` file that has comments for each specific SO -* Start the Grid running the following command inside the `grid` folder - * `docker-compose up` +* Pull the images for Chrome Edge and Firefox - Optional + * Images are pulled if not available and initial test execution will be slow + * `docker pull selenium-standalog-chrome` + * `docker pull selenium-standalog-firefox` + * `docker pull selenium/standalone-edge` + * If you are using a MacBook with either M1 or M2 chip you must check the following experimental feature in Docker + Desktop: Settings -> Features in development -> Use Rosetta for x86/amd64 emulation on Apple Silicon +* Pay attention to the `grid/config.toml` file that has comments for each specific SO +* Start the Grid by running the following command inside the `grid` folder + * `docker-compose up` +* Run the project using the following command + +```shell +mvn test -Pweb-execution -Dsuite=selenium-grid -Dtarget=selenium-grid -Dheadless=true +``` + +* Open the [Selenium Grid] page to see the node status ### Configuration files -This project uses a library called [Owner](http://owner.aeonbits.org/). You can find the class related to the property + +This project uses a library called [Owner](http://owner.aeonbits.org/). You can find the class related to the property file reader in the following classes: + * [Configuration](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/config/Configuration.java) * [ConfigurationManager](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/config/ConfigurationManager.java) There are 3 properties (configuration) files located on `src/test/java/resources/`: -* `general.properties`: general configuration as the target execution, base url, timeout, and faker locale + +* `general.properties`: general configuration as the target execution, browser, base url, timeout, and faker locale * `grid.properties`: url and port for the Selenium grid usage -* `local.properties`: browser to use in the local execution -The properties were divided into three different ones to better separate the responsibilities and enable the changes easy -without have a lot of properties inside a single file. +The properties were divided into three different ones to better separate the responsibilities and enable the changes +easy without having a lot of properties inside a single file. ### Test Data Factory + Is the utilization of the Factory design pattern with the Fluent Builder to generate dynamic data. -The [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) +The [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) has only one factory `createBookingData` returning a `Booking` object with dynamic data. This dynamic data is generated by JavaFaker filling all the fields using the Build pattern. -The [Booking](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/Booking.java) is the plain Java objects -and the [BookingBuilder](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/BookingBuilder.java) is the builder class. +The [Booking](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/Booking.java) +is the plain Java objects +and +the [BookingBuilder](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/model/BookingBuilder.java) +is the builder class. -You can ses the use of the Builder pattern in the [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) class. +You can see the usage of the Builder pattern in +the [BookingDataFactory](https://github.com/eliasnogueira/selenium-java-lean-test-achitecture/blob/master/src/main/java/com/eliasnogueira/data/BookingDataFactory.java) +class. Reading reference: https://reflectoring.io/objectmother-fluent-builder ### Profiles executors on pom.xml -There is a profile called `web-execution` created to execute the test suite `local.xml` inside `src/test/resources/suites` folder. +There is a profile called `web-execution` created to execute the test suite `local.xml` +inside `src/test/resources/suites` folder. To execute this suite, via the command line you can call the parameter `-P` and the profile id. Eg: executing the multi_browser suite + ``` bash -mvn test -Pweb-execution -Dtestng.dtd.http=true +mvn test -Pweb-execution ``` If you have more than one suite on _src/test/resources/suites_ folder you can parameterize the xml file name. @@ -211,33 +311,36 @@ To do this you need: * Create a property on `pom.xml` called _suite_ ```xml - - local - + + + local + ``` * Change the profile id ```xml + - web-execution + web-execution ``` * Replace the xml file name to `${suite}` on the profile ```xml + - - src/test/resources/suites/${suite}.xml - + + src/test/resources/suites/${suite}.xml + ``` * Use `-Dsuite=suite_name` to call the suite ````bash -mvn test -Pweb-execution -Dsuite=parallel -Dtestng.dtd.http=true +mvn test -Pweb-execution -Dsuite=suite_name ```` ### Pipeline as a code @@ -246,4 +349,4 @@ The two files of the pipeline as a code are inside `pipeline_as_code` folder. * GitHub Actions to use it inside the GitHub located at `.github\workflows` * Jenkins: `Jenkinsfile` to be used on a Jenkins pipeline located at `pipeline_as_code` -* GitLab CI: `.gitlab-ci.yml` to be used on a GitLab CI `pipeline_as_code` \ No newline at end of file +* GitLab CI: `.gitlab-ci.yml` to be used on a GitLab CI `pipeline_as_code` diff --git a/example_filed_test_with_report.gif b/assets/example_filed_test_with_report.gif similarity index 100% rename from example_filed_test_with_report.gif rename to assets/example_filed_test_with_report.gif diff --git a/selenium-grid-execution.gif b/assets/selenium-grid-execution.gif similarity index 100% rename from selenium-grid-execution.gif rename to assets/selenium-grid-execution.gif diff --git a/grid/config.toml b/grid/config.toml index 9caf674..2d961ae 100644 --- a/grid/config.toml +++ b/grid/config.toml @@ -2,21 +2,27 @@ # Configs have a mapping between the Docker image to use and the capabilities that need to be matched to # start a container with the given image. configs = [ - "selenium/standalone-firefox:4.0.0-20211013", "{\"browserName\": \"firefox\"}", - "selenium/standalone-chrome:4.0.0-20211013", "{\"browserName\": \"chrome\"}" + "selenium/standalone-firefox:latest", "{\"browserName\": \"firefox\"}", + "selenium/standalone-chrome:latest", "{\"browserName\": \"chrome\"}" ] +host-config-keys = ["Binds"] + # URL for connecting to the docker daemon -# host.docker.internal works for macOS and Windows. -# Linux could use --net=host in the `docker run` instruction or 172.17.0.1 in the URI below. -# To have Docker listening through tcp on macOS, install socat and run the following command -# socat -4 TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock +# Most simple approach, leave it as http://127.0.0.1:2375, and mount /var/run/docker.sock. +# 127.0.0.1 is used because internally the container uses socat when /var/run/docker.sock is mounted +# If var/run/docker.sock is not mounted: +# Windows: make sure Docker Desktop exposes the daemon via tcp, and use http://host.docker.internal:2375. +# macOS: install socat and run the following command, socat -4 TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock, +# then use http://host.docker.internal:2375. +# Linux: varies from machine to machine, please mount /var/run/docker.sock. If this does not work, please create an issue. + url = "http://host.docker.internal:2375/" # Docker image used for video recording -video-image = "selenium/video:ffmpeg-4.3.1-20211013" +video-image = "selenium/video:latest" # Uncomment the following section if you are running the node on a separate VM # Fill out the placeholders with appropriate values #[server] #host = -#port = \ No newline at end of file +#port = diff --git a/grid/docker-compose.yml b/grid/docker-compose.yml index 104fd1a..7163cac 100644 --- a/grid/docker-compose.yml +++ b/grid/docker-compose.yml @@ -1,13 +1,11 @@ -# To execute this docker-compose yml file use `docker-compose -f docker-compose-v3-dynamic-grid.yml up` -# Add the `-d` flag at the end for detached execution -# To stop the execution, hit Ctrl+C, and then `docker-compose -f docker-compose-v3-dynamic-grid.yml down` -version: "3" services: node-docker: - image: selenium/node-docker:4.0.0-20211013 + image: selenium/node-docker:latest volumes: - ./assets:/opt/selenium/assets - ./config.toml:/opt/bin/config.toml + - ~/Downloads:/home/seluser/Downloads + - /var/run/docker.sock:/var/run/docker.sock depends_on: - selenium-hub environment: @@ -16,9 +14,9 @@ services: - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 selenium-hub: - image: selenium/hub:4.0.0-20211013 + image: selenium/hub:latest container_name: selenium-hub ports: - "4442:4442" - "4443:4443" - - "4444:4444" \ No newline at end of file + - "4444:4444" diff --git a/pom.xml b/pom.xml index bc10942..d3faf76 100644 --- a/pom.xml +++ b/pom.xml @@ -5,35 +5,39 @@ 4.0.0 com.eliasnogueira - selenium-java-lean-test-achitecture - 2.0.0 + selenium-java-lean-test-architecture + 3.8.0 - scm:git@github.com:eliasnogueira/selenium-java-lean-test-achitecture.git - scm:git@github.com:eliasnogueira/selenium-java-lean-test-achitecture.git + scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git + scm:git@github.com:eliasnogueira/selenium-java-lean-test-architecture.git + + 24 UTF-8 UTF-8 - 11 - 3.0.0-M5 - 3.9.0 - - 1.9.6 - 4.1.2 - 7.5 - 3.22.0 - 1.0.2 - 2.17.1 - 5.0.3 + 3.5.3 + 3.14.0 + + 1.9.24 + 4.35.0 + 7.11.0 + 3.27.4 + 2.4.4 + 2.25.1 1.0.12 - 2.17.2 - 2.11.2 + 2.33.0 + 2.29.1 + 2.29.1 + 2.15.2 1.0.0 https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline + 1.21.3 + 2.0.17 local @@ -58,9 +62,9 @@ - com.github.javafaker - javafaker - ${javafaker.version} + net.datafaker + datafaker + ${datafaker.version} @@ -87,28 +91,16 @@ ${owner.version} - - io.github.bonigarcia - webdrivermanager - ${webdrivermanager.version} - - - com.google.guava - guava - - - - io.qameta.allure allure-testng - ${allure.version} + ${allure-testng.version} io.qameta.allure allure-attachments - ${allure.version} + ${allure-attachments.version} @@ -123,9 +115,28 @@ + + org.testcontainers + selenium + ${testcontainers.selenium.version} + + + org.apache.commons + commons-compress + + + + + + org.slf4j + slf4j-simple + ${slf4j-simple.version} + test + + - + web-execution @@ -144,6 +155,7 @@ + @@ -158,9 +170,6 @@ -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" false - - target/allure-results - @@ -186,12 +195,11 @@ maven-compiler-plugin ${maven-compiler-plugin.version} - ${java-compiler.version} - ${java-compiler.version} + ${java-compiler.version} - \ No newline at end of file + diff --git a/src/main/java/com/eliasnogueira/config/Configuration.java b/src/main/java/com/eliasnogueira/config/Configuration.java index 56b4db9..dda4233 100644 --- a/src/main/java/com/eliasnogueira/config/Configuration.java +++ b/src/main/java/com/eliasnogueira/config/Configuration.java @@ -32,8 +32,7 @@ @Config.Sources({ "system:properties", "classpath:general.properties", - "classpath:local.properties", - "classpath:grid.properties"}) + "classpath:selenium-grid.properties"}) public interface Configuration extends Config { @Key("target") diff --git a/src/main/java/com/eliasnogueira/config/ConfigurationManager.java b/src/main/java/com/eliasnogueira/config/ConfigurationManager.java index ba842b4..ce15843 100644 --- a/src/main/java/com/eliasnogueira/config/ConfigurationManager.java +++ b/src/main/java/com/eliasnogueira/config/ConfigurationManager.java @@ -35,4 +35,3 @@ public static Configuration configuration() { return ConfigCache.getOrCreate(Configuration.class); } } - diff --git a/src/main/java/com/eliasnogueira/exceptions/TargetNotValidException.java b/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java similarity index 65% rename from src/main/java/com/eliasnogueira/exceptions/TargetNotValidException.java rename to src/main/java/com/eliasnogueira/data/changeless/BrowserData.java index 0dbf149..810154d 100644 --- a/src/main/java/com/eliasnogueira/exceptions/TargetNotValidException.java +++ b/src/main/java/com/eliasnogueira/data/changeless/BrowserData.java @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2021 Elias Nogueira + * Copyright (c) 2022 Elias Nogueira * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,17 @@ * SOFTWARE. */ -package com.eliasnogueira.exceptions; +package com.eliasnogueira.data.changeless; -public class TargetNotValidException extends IllegalStateException { +public final class BrowserData { - public TargetNotValidException(String target) { - super(String.format("Target %s not supported. Use either local or gird", target)); + private BrowserData() { } + public static final String START_MAXIMIZED = "--start-maximized"; + public static final String DISABLE_INFOBARS = "--disable-infobars"; + public static final String DISABLE_NOTIFICATIONS = "--disable-notifications"; + public static final String REMOTE_ALLOW_ORIGINS = "--remote-allow-origins=*"; + public static final String GENERIC_HEADLESS = "-headless"; + public static final String CHROME_HEADLESS = "--headless=new"; } diff --git a/src/main/java/com/eliasnogueira/data/BookingDataFactory.java b/src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java similarity index 69% rename from src/main/java/com/eliasnogueira/data/BookingDataFactory.java rename to src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java index 75eb129..c1dc321 100644 --- a/src/main/java/com/eliasnogueira/data/BookingDataFactory.java +++ b/src/main/java/com/eliasnogueira/data/dynamic/BookingDataFactory.java @@ -22,36 +22,34 @@ * SOFTWARE. */ -package com.eliasnogueira.data; +package com.eliasnogueira.data.dynamic; -import com.github.javafaker.Faker; import com.eliasnogueira.enums.RoomType; -import java.util.Locale; -import java.util.Random; import com.eliasnogueira.model.Booking; -import com.eliasnogueira.model.BookingBuilder; +import net.datafaker.Faker; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Locale; + import static com.eliasnogueira.config.ConfigurationManager.configuration; -public class BookingDataFactory { +public final class BookingDataFactory { - private final Faker faker; + private static final Faker faker = new Faker(new Locale.Builder().setLanguageTag(configuration().faker()).build()); private static final Logger logger = LogManager.getLogger(BookingDataFactory.class); - public BookingDataFactory() { - faker = new Faker(new Locale(configuration().faker())); + private BookingDataFactory() { } - public Booking createBookingData() { - Booking booking = new BookingBuilder(). + public static Booking createBookingData() { + var booking = new Booking.BookingBuilder(). email(faker.internet().emailAddress()). country(returnRandomCountry()). password(faker.internet().password()). dailyBudget(returnDailyBudget()). newsletter(faker.bool().bool()). - roomType(RoomType.getRandom()). + roomType(faker.options().option(RoomType.class)). roomDescription(faker.lorem().paragraph()). build(); @@ -59,15 +57,11 @@ public Booking createBookingData() { return booking; } - private String returnRandomCountry() { - return returnRandomItemOnArray(new String[]{"Belgium", "Brazil", "Netherlands"}); - } - - private String returnDailyBudget() { - return returnRandomItemOnArray(new String[]{"$100", "$100 - $499", "$499 - $999", "$999+"}); + private static String returnRandomCountry() { + return faker.options().option("Belgium", "Brazil", "Netherlands"); } - private String returnRandomItemOnArray(String[] array) { - return array[(new Random().nextInt(array.length))]; + private static String returnDailyBudget() { + return faker.options().option("$100", "$100 - $499", "$499 - $999", "$999+"); } } diff --git a/src/main/java/com/eliasnogueira/driver/BrowserFactory.java b/src/main/java/com/eliasnogueira/driver/BrowserFactory.java index 25ecfb5..0b7f43f 100644 --- a/src/main/java/com/eliasnogueira/driver/BrowserFactory.java +++ b/src/main/java/com/eliasnogueira/driver/BrowserFactory.java @@ -25,8 +25,6 @@ package com.eliasnogueira.driver; import com.eliasnogueira.exceptions.HeadlessNotSupportedException; -import io.github.bonigarcia.wdm.WebDriverManager; -import io.github.bonigarcia.wdm.config.DriverManagerType; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; @@ -34,80 +32,107 @@ import org.openqa.selenium.edge.EdgeOptions; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.firefox.FirefoxOptions; -import org.openqa.selenium.ie.InternetExplorerDriver; -import org.openqa.selenium.ie.InternetExplorerOptions; -import org.openqa.selenium.opera.OperaDriver; -import org.openqa.selenium.opera.OperaOptions; import org.openqa.selenium.remote.AbstractDriverOptions; +import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.safari.SafariDriver; import org.openqa.selenium.safari.SafariOptions; +import org.testcontainers.containers.BrowserWebDriverContainer; import static com.eliasnogueira.config.ConfigurationManager.configuration; +import static com.eliasnogueira.data.changeless.BrowserData.CHROME_HEADLESS; +import static com.eliasnogueira.data.changeless.BrowserData.DISABLE_INFOBARS; +import static com.eliasnogueira.data.changeless.BrowserData.DISABLE_NOTIFICATIONS; +import static com.eliasnogueira.data.changeless.BrowserData.GENERIC_HEADLESS; +import static com.eliasnogueira.data.changeless.BrowserData.REMOTE_ALLOW_ORIGINS; +import static com.eliasnogueira.data.changeless.BrowserData.START_MAXIMIZED; import static java.lang.Boolean.TRUE; public enum BrowserFactory { CHROME { @Override - public WebDriver createDriver() { - WebDriverManager.getInstance(DriverManagerType.CHROME).setup(); - + public WebDriver createLocalDriver() { return new ChromeDriver(getOptions()); } + @Override + public WebDriver createTestContainerDriver() { + BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new ChromeOptions()); + driverContainer.start(); + + return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new ChromeOptions()); + } + @Override public ChromeOptions getOptions() { - ChromeOptions chromeOptions = new ChromeOptions(); + var chromeOptions = new ChromeOptions(); chromeOptions.addArguments(START_MAXIMIZED); - chromeOptions.addArguments("--disable-infobars"); - chromeOptions.addArguments("--disable-notifications"); - chromeOptions.setHeadless(configuration().headless()); + chromeOptions.addArguments(DISABLE_INFOBARS); + chromeOptions.addArguments(DISABLE_NOTIFICATIONS); + chromeOptions.addArguments(REMOTE_ALLOW_ORIGINS); + + if (configuration().headless()) chromeOptions.addArguments(CHROME_HEADLESS); return chromeOptions; } }, FIREFOX { @Override - public WebDriver createDriver() { - WebDriverManager.getInstance(DriverManagerType.FIREFOX).setup(); - + public WebDriver createLocalDriver() { return new FirefoxDriver(getOptions()); } + @Override + public WebDriver createTestContainerDriver() { + BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new FirefoxOptions()); + driverContainer.start(); + + return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new FirefoxOptions()); + } + @Override public FirefoxOptions getOptions() { - FirefoxOptions firefoxOptions = new FirefoxOptions(); + var firefoxOptions = new FirefoxOptions(); firefoxOptions.addArguments(START_MAXIMIZED); - firefoxOptions.setHeadless(configuration().headless()); + + if (configuration().headless()) firefoxOptions.addArguments(GENERIC_HEADLESS); return firefoxOptions; } }, EDGE { @Override - public WebDriver createDriver() { - WebDriverManager.getInstance(DriverManagerType.EDGE).setup(); - + public WebDriver createLocalDriver() { return new EdgeDriver(getOptions()); } + public WebDriver createTestContainerDriver() { + BrowserWebDriverContainer driverContainer = new BrowserWebDriverContainer<>().withCapabilities(new EdgeOptions()); + driverContainer.start(); + + return new RemoteWebDriver(driverContainer.getSeleniumAddress(), new EdgeOptions()); + } + @Override public EdgeOptions getOptions() { - EdgeOptions edgeOptions = new EdgeOptions(); + var edgeOptions = new EdgeOptions(); edgeOptions.addArguments(START_MAXIMIZED); - edgeOptions.setHeadless(configuration().headless()); + + if (configuration().headless()) edgeOptions.addArguments(GENERIC_HEADLESS); return edgeOptions; } }, SAFARI { @Override - public WebDriver createDriver() { - WebDriverManager.getInstance(DriverManagerType.SAFARI).setup(); - + public WebDriver createLocalDriver() { return new SafariDriver(getOptions()); } + public WebDriver createTestContainerDriver() { + throw new IllegalArgumentException("Browser Safari not supported on TestContainers yet"); + } + @Override public SafariOptions getOptions() { - SafariOptions safariOptions = new SafariOptions(); + var safariOptions = new SafariOptions(); safariOptions.setAutomaticInspection(false); if (TRUE.equals(configuration().headless())) @@ -115,51 +140,24 @@ public SafariOptions getOptions() { return safariOptions; } - }, OPERA { - @Override - public WebDriver createDriver() { - WebDriverManager.getInstance(DriverManagerType.OPERA).setup(); - - return new OperaDriver(getOptions()); - } - - @Override - public OperaOptions getOptions() { - OperaOptions operaOptions = new OperaOptions(); - operaOptions.addArguments(START_MAXIMIZED); - operaOptions.addArguments("--disable-infobars"); - operaOptions.addArguments("--disable-notifications"); - - if (TRUE.equals(configuration().headless())) - throw new HeadlessNotSupportedException(operaOptions.getBrowserName()); - - return operaOptions; - } - }, IE { - @Override - public WebDriver createDriver() { - WebDriverManager.getInstance(DriverManagerType.IEXPLORER).setup(); - - return new InternetExplorerDriver(getOptions()); - } - - @Override - public InternetExplorerOptions getOptions() { - InternetExplorerOptions internetExplorerOptions = new InternetExplorerOptions(); - internetExplorerOptions.ignoreZoomSettings(); - internetExplorerOptions.takeFullPageScreenshot(); - internetExplorerOptions.introduceFlakinessByIgnoringSecurityDomains(); - - if (TRUE.equals(configuration().headless())) - throw new HeadlessNotSupportedException(internetExplorerOptions.getBrowserName()); - - return internetExplorerOptions; - } }; - private static final String START_MAXIMIZED = "--start-maximized"; - - public abstract WebDriver createDriver(); + /** + * Used to run local tests where the WebDriverManager will take care of the driver + * + * @return a new WebDriver instance based on the browser set + */ + public abstract WebDriver createLocalDriver(); + /** + * @return a new AbstractDriverOptions instance based on the browser set + */ public abstract AbstractDriverOptions getOptions(); + + /** + * Used to run the remote test execution using Testcontainers + * + * @return a new WebDriver instance based on the browser set + */ + public abstract WebDriver createTestContainerDriver(); } diff --git a/src/main/java/com/eliasnogueira/driver/DriverManager.java b/src/main/java/com/eliasnogueira/driver/DriverManager.java index f28102e..680076f 100644 --- a/src/main/java/com/eliasnogueira/driver/DriverManager.java +++ b/src/main/java/com/eliasnogueira/driver/DriverManager.java @@ -24,7 +24,6 @@ package com.eliasnogueira.driver; -import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; @@ -48,10 +47,11 @@ public static void quit() { } public static String getInfo() { - Capabilities cap = ((RemoteWebDriver) DriverManager.getDriver()).getCapabilities(); + var cap = ((RemoteWebDriver) DriverManager.getDriver()).getCapabilities(); String browserName = cap.getBrowserName(); String platform = cap.getPlatformName().toString(); String version = cap.getBrowserVersion(); + return String.format("browser: %s v: %s platform: %s", browserName, version, platform); } } diff --git a/src/main/java/com/eliasnogueira/driver/TargetFactory.java b/src/main/java/com/eliasnogueira/driver/TargetFactory.java index 36c598c..3919b61 100644 --- a/src/main/java/com/eliasnogueira/driver/TargetFactory.java +++ b/src/main/java/com/eliasnogueira/driver/TargetFactory.java @@ -24,55 +24,47 @@ package com.eliasnogueira.driver; -import com.eliasnogueira.exceptions.TargetNotValidException; +import com.eliasnogueira.enums.Target; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; -import java.net.URL; +import java.net.URI; import static com.eliasnogueira.config.ConfigurationManager.configuration; +import static com.eliasnogueira.driver.BrowserFactory.valueOf; +import static java.lang.String.format; public class TargetFactory { private static final Logger logger = LogManager.getLogger(TargetFactory.class); public WebDriver createInstance(String browser) { - Target target = Target.valueOf(configuration().target().toUpperCase()); - WebDriver webdriver; + Target target = Target.get(configuration().target().toUpperCase()); - switch (target) { - case LOCAL: - webdriver = BrowserFactory.valueOf(browser.toUpperCase()).createDriver(); - break; - case REMOTE: - webdriver = createRemoteInstance(BrowserFactory.valueOf(browser.toUpperCase()).getOptions()); - break; - default: - throw new TargetNotValidException(target.toString()); - } - return webdriver; + return switch (target) { + case LOCAL -> valueOf(configuration().browser().toUpperCase()).createLocalDriver(); + case LOCAL_SUITE -> valueOf(browser.toUpperCase()).createLocalDriver(); + case SELENIUM_GRID -> createRemoteInstance(valueOf(browser.toUpperCase()).getOptions()); + case TESTCONTAINERS -> valueOf(configuration().browser().toUpperCase()).createTestContainerDriver(); + }; } private RemoteWebDriver createRemoteInstance(MutableCapabilities capability) { RemoteWebDriver remoteWebDriver = null; try { - String gridURL = String.format("http://%s:%s", configuration().gridUrl(), configuration().gridPort()); + String gridURL = format("http://%s:%s", configuration().gridUrl(), configuration().gridPort()); - remoteWebDriver = new RemoteWebDriver(new URL(gridURL), capability); + remoteWebDriver = new RemoteWebDriver(URI.create(gridURL).toURL(), capability); } catch (java.net.MalformedURLException e) { logger.error("Grid URL is invalid or Grid is not available"); - logger.error(String.format("Browser: %s", capability.getBrowserName()), e); + logger.error("Browser: {}", capability.getBrowserName(), e); } catch (IllegalArgumentException e) { - logger.error(String.format("Browser %s is not valid or recognized", capability.getBrowserName()), e); + logger.error("Browser {} is not valid or recognized", capability.getBrowserName(), e); } return remoteWebDriver; } - - enum Target { - LOCAL, REMOTE - } } diff --git a/src/main/java/com/eliasnogueira/enums/RoomType.java b/src/main/java/com/eliasnogueira/enums/RoomType.java index 80432cc..d7b0980 100644 --- a/src/main/java/com/eliasnogueira/enums/RoomType.java +++ b/src/main/java/com/eliasnogueira/enums/RoomType.java @@ -24,9 +24,9 @@ package com.eliasnogueira.enums; -import java.util.Random; +import java.util.function.Supplier; -public enum RoomType { +public enum RoomType implements Supplier { SINGLE("Single"), FAMILY("Family"), BUSINESS("Business"); @@ -36,12 +36,8 @@ public enum RoomType { this.value = value; } - public static RoomType getRandom() { - return values()[new Random().nextInt(values().length)]; - } - @Override - public String toString() { - return value; + public String get() { + return this.value; } -} \ No newline at end of file +} diff --git a/src/main/java/com/eliasnogueira/enums/Target.java b/src/main/java/com/eliasnogueira/enums/Target.java new file mode 100644 index 0000000..d9dfa4e --- /dev/null +++ b/src/main/java/com/eliasnogueira/enums/Target.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2022 Elias Nogueira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.eliasnogueira.enums; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + +public enum Target { + + LOCAL("local"), LOCAL_SUITE("local-suite"), SELENIUM_GRID("selenium-grid"), + TESTCONTAINERS("testcontainers"); + + private final String value; + private static final Map ENUM_MAP; + + Target(String value) { + this.value = value; + } + + static { + Map map = stream(Target.values()) + .collect(toMap(instance -> instance.value.toLowerCase(), instance -> instance, (_, b) -> b, ConcurrentHashMap::new)); + ENUM_MAP = Collections.unmodifiableMap(map); + } + + public static Target get(String value) { + if (!ENUM_MAP.containsKey(value.toLowerCase())) + throw new IllegalArgumentException(String.format("Value %s not valid. Use one of the TARGET enum values", value)); + + return ENUM_MAP.get(value.toLowerCase()); + } +} diff --git a/src/main/java/com/eliasnogueira/model/Booking.java b/src/main/java/com/eliasnogueira/model/Booking.java index 812a591..5348d03 100644 --- a/src/main/java/com/eliasnogueira/model/Booking.java +++ b/src/main/java/com/eliasnogueira/model/Booking.java @@ -26,90 +26,56 @@ import com.eliasnogueira.enums.RoomType; -public class Booking { - - private String email; - private String country; - private String password; - private String dailyBudget; - private Boolean newsletter; - private RoomType roomType; - private String roomDescription; - - public Booking(String email, String country, String password, String dailyBudget, Boolean newsletter, - RoomType roomType, String roomDescription) { - this.email = email; - this.country = country; - this.password = password; - this.dailyBudget = dailyBudget; - this.newsletter = newsletter; - this.roomType = roomType; - this.roomDescription = roomDescription; - } - - public Booking() { - } - - public String getEmail() { - return this.email; - } - - public String getCountry() { - return this.country; - } - - public String getPassword() { - return this.password; - } - - public String getDailyBudget() { - return this.dailyBudget; - } - - public Boolean getNewsletter() { - return this.newsletter; - } - - public RoomType getRoomType() { - return this.roomType; - } - - public String getRoomDescription() { - return this.roomDescription; +public record Booking(String email, String country, String password, String dailyBudget, Boolean newsletter, + RoomType roomType, String roomDescription) { + + public static final class BookingBuilder { + + private String email; + private String country; + private String password; + private String dailyBudget; + private Boolean newsletter; + private RoomType roomType; + private String roomDescription; + + public BookingBuilder email(String email) { + this.email = email; + return this; + } + + public BookingBuilder country(String country) { + this.country = country; + return this; + } + + public BookingBuilder password(String password) { + this.password = password; + return this; + } + + public BookingBuilder dailyBudget(String dailyBudget) { + this.dailyBudget = dailyBudget; + return this; + } + + public BookingBuilder newsletter(Boolean newsletter) { + this.newsletter = newsletter; + return this; + } + + public BookingBuilder roomType(RoomType roomType) { + this.roomType = roomType; + return this; + } + + public BookingBuilder roomDescription(String roomDescription) { + this.roomDescription = roomDescription; + return this; + } + + public Booking build() { + return new Booking(email, country, password, dailyBudget, newsletter, roomType, roomDescription); + } } - - public void setEmail(String email) { - this.email = email; - } - - public void setCountry(String country) { - this.country = country; - } - - public void setPassword(String password) { - this.password = password; - } - - public void setDailyBudget(String dailyBudget) { - this.dailyBudget = dailyBudget; - } - - public void setNewsletter(Boolean newsletter) { - this.newsletter = newsletter; - } - - public void setRoomType(RoomType roomType) { - this.roomType = roomType; - } - - public void setRoomDescription(String roomDescription) { - this.roomDescription = roomDescription; - } - - public String toString() { - return "Booking(email=" + this.getEmail() + ", country=" + this.getCountry() + ", dailyBudget=" + - this.getDailyBudget() + ", newsletter=" + this.getNewsletter() + ", roomType=" - + this.getRoomType() + ", roomDescription=" + this.getRoomDescription() + ")"; - } - } diff --git a/src/main/java/com/eliasnogueira/model/BookingBuilder.java b/src/main/java/com/eliasnogueira/model/BookingBuilder.java deleted file mode 100644 index 82da658..0000000 --- a/src/main/java/com/eliasnogueira/model/BookingBuilder.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018 Elias Nogueira - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.eliasnogueira.model; - -import com.eliasnogueira.enums.RoomType; - -public class BookingBuilder { - - private String email; - private String country; - private String password; - private String dailyBudget; - private Boolean newsletter; - private RoomType roomType; - private String roomDescription; - - public BookingBuilder email(String email) { - this.email = email; - return this; - } - - public BookingBuilder country(String country) { - this.country = country; - return this; - } - - public BookingBuilder password(String password) { - this.password = password; - return this; - } - - public BookingBuilder dailyBudget(String dailyBudget) { - this.dailyBudget = dailyBudget; - return this; - } - - public BookingBuilder newsletter(Boolean newsletter) { - this.newsletter = newsletter; - return this; - } - - public BookingBuilder roomType(RoomType roomType) { - this.roomType = roomType; - return this; - } - - public BookingBuilder roomDescription(String roomDescription) { - this.roomDescription = roomDescription; - return this; - } - - public Booking build() { - return new Booking(email, country, password, dailyBudget, newsletter, roomType, roomDescription); - } -} \ No newline at end of file diff --git a/src/main/java/com/eliasnogueira/page/booking/RoomPage.java b/src/main/java/com/eliasnogueira/page/booking/RoomPage.java index 05763b0..5b14b06 100644 --- a/src/main/java/com/eliasnogueira/page/booking/RoomPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/RoomPage.java @@ -25,7 +25,6 @@ package com.eliasnogueira.page.booking; import com.eliasnogueira.driver.DriverManager; -import com.eliasnogueira.enums.RoomType; import com.eliasnogueira.page.booking.common.NavigationPage; import io.qameta.allure.Step; import org.openqa.selenium.By; @@ -33,7 +32,7 @@ public class RoomPage extends NavigationPage { @Step - public void selectRoomType(RoomType room) { + public void selectRoomType(String room) { DriverManager.getDriver().findElement(By.xpath("//h6[text()='" + room + "']")).click(); } } diff --git a/src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java b/src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java index 46be648..ceafb1d 100644 --- a/src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java +++ b/src/main/java/com/eliasnogueira/page/booking/common/NavigationPage.java @@ -45,11 +45,6 @@ public void next() { next.click(); } - @Step - public void previous() { - previous.click(); - } - @Step public void finish() { finish.click(); diff --git a/src/main/java/com/eliasnogueira/report/AllureManager.java b/src/main/java/com/eliasnogueira/report/AllureManager.java index 8dd5891..2180e2e 100644 --- a/src/main/java/com/eliasnogueira/report/AllureManager.java +++ b/src/main/java/com/eliasnogueira/report/AllureManager.java @@ -24,14 +24,14 @@ package com.eliasnogueira.report; -import com.eliasnogueira.driver.DriverManager; +import com.eliasnogueira.enums.Target; import com.github.automatedowl.tools.AllureEnvironmentWriter; import com.google.common.collect.ImmutableMap; -import io.qameta.allure.Attachment; -import org.openqa.selenium.TakesScreenshot; + +import java.util.HashMap; +import java.util.Map; import static com.eliasnogueira.config.ConfigurationManager.configuration; -import static org.openqa.selenium.OutputType.BYTES; public class AllureManager { @@ -39,27 +39,20 @@ private AllureManager() { } public static void setAllureEnvironmentInformation() { - AllureEnvironmentWriter.allureEnvironmentWriter( - ImmutableMap.builder(). - put("Test URL", configuration().url()). - put("Target execution", configuration().target()). - put("Global timeout", String.valueOf(configuration().timeout())). - put("Headless mode", String.valueOf(configuration().headless())). - put("Faker locale", configuration().faker()). - put("Local browser", configuration().browser()). - put("Grid URL", configuration().gridUrl()). - put("Grid port", configuration().gridPort()). - build()); - } - - @Attachment(value = "Failed test screenshot", type = "image/png") - public static byte[] takeScreenshotToAttachOnAllureReport() { - return ((TakesScreenshot) DriverManager.getDriver()).getScreenshotAs(BYTES); + var basicInfo = new HashMap<>(Map.of( + "Test URL", configuration().url(), + "Target execution", configuration().target(), + "Global timeout", String.valueOf(configuration().timeout()), + "Headless mode", String.valueOf(configuration().headless()), + "Faker locale", configuration().faker(), + "Local browser", configuration().browser() + )); + + if (configuration().target().equals(Target.SELENIUM_GRID.name())) { + var gridMap = Map.of("Grid URL", configuration().gridUrl(), "Grid port", configuration().gridPort()); + basicInfo.putAll(gridMap); + } + + AllureEnvironmentWriter.allureEnvironmentWriter(ImmutableMap.copyOf(basicInfo)); } - - @Attachment(value = "Browser information", type = "text/plain") - public static String addBrowserInformationOnAllureReport() { - return DriverManager.getInfo(); - } - } diff --git a/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java b/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java new file mode 100644 index 0000000..4d09ef2 --- /dev/null +++ b/src/main/java/com/eliasnogueira/report/AllureTestLifecycleListener.java @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2024 Elias Nogueira + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.eliasnogueira.report; + +import com.eliasnogueira.driver.DriverManager; +import io.qameta.allure.Attachment; +import io.qameta.allure.listener.TestLifecycleListener; +import io.qameta.allure.model.TestResult; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; + +import static io.qameta.allure.model.Status.BROKEN; +import static io.qameta.allure.model.Status.FAILED; + +/* + * Approach implemented using the https://github.com/biczomate/allure-testng7.5-attachment-example as reference + */ +public class AllureTestLifecycleListener implements TestLifecycleListener { + + public AllureTestLifecycleListener() { + } + + @Attachment(value = "Page Screenshot", type = "image/png") + public byte[] saveScreenshot(WebDriver driver) { + return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + } + + @Override + public void beforeTestStop(TestResult result) { + if (FAILED == result.getStatus() || BROKEN == result.getStatus()) { + saveScreenshot(DriverManager.getDriver()); + } + } +} diff --git a/src/main/resources/log4j2.properties b/src/main/resources/log4j2.properties index 6b21586..d41ecde 100644 --- a/src/main/resources/log4j2.properties +++ b/src/main/resources/log4j2.properties @@ -16,4 +16,4 @@ appender.file.layout.pattern=[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - rootLogger.level = info rootLogger.appenderRefs = stdout rootLogger.appenderRef.stdout.ref = STDOUT -rootLogger.appenderRef.file.ref = LOGFILE \ No newline at end of file +rootLogger.appenderRef.file.ref = LOGFILE diff --git a/src/test/java/com/eliasnogueira/BaseWeb.java b/src/test/java/com/eliasnogueira/BaseWeb.java index 678bc58..cd5c894 100644 --- a/src/test/java/com/eliasnogueira/BaseWeb.java +++ b/src/test/java/com/eliasnogueira/BaseWeb.java @@ -31,13 +31,11 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeSuite; -import org.testng.annotations.Listeners; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; import static com.eliasnogueira.config.ConfigurationManager.configuration; -@Listeners({TestListener.class}) public abstract class BaseWeb { @BeforeSuite diff --git a/src/test/java/com/eliasnogueira/TestListener.java b/src/test/java/com/eliasnogueira/TestListener.java deleted file mode 100644 index a0fa483..0000000 --- a/src/test/java/com/eliasnogueira/TestListener.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2018 Elias Nogueira - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package com.eliasnogueira; - -import com.eliasnogueira.report.AllureManager; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.testng.ITestContext; -import org.testng.ITestListener; -import org.testng.ITestResult; - -public class TestListener implements ITestListener { - - private static final Logger logger = LogManager.getLogger(TestListener.class); - - @Override - public void onTestStart(ITestResult result) { - // empty - } - - @Override - public void onTestSuccess(ITestResult result) { - // empty - } - - @Override - public void onTestFailure(ITestResult result) { - failTest(result); - } - - @Override - public void onTestSkipped(ITestResult result) { - logger.error(result.getThrowable()); - } - - @Override - public void onTestFailedButWithinSuccessPercentage(ITestResult result) { - // empty - } - - @Override - public void onStart(ITestContext context) { - // empty - } - - @Override - public void onFinish(ITestContext context) { - // empty - } - - private void failTest(ITestResult iTestResult) { - logger.error(iTestResult.getTestClass().getName()); - logger.error(iTestResult.getThrowable()); - - AllureManager.takeScreenshotToAttachOnAllureReport(); - } -} diff --git a/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java b/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java index ee2dab1..7471352 100644 --- a/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java +++ b/src/test/java/com/eliasnogueira/test/BookRoomWebTest.java @@ -24,8 +24,7 @@ package com.eliasnogueira.test; import com.eliasnogueira.BaseWeb; -import com.eliasnogueira.data.BookingDataFactory; -import com.eliasnogueira.model.Booking; +import com.eliasnogueira.data.dynamic.BookingDataFactory; import com.eliasnogueira.page.booking.AccountPage; import com.eliasnogueira.page.booking.DetailPage; import com.eliasnogueira.page.booking.RoomPage; @@ -37,22 +36,22 @@ public class BookRoomWebTest extends BaseWeb { @Test(description = "Book a room") public void bookARoom() { - Booking bookingInformation = new BookingDataFactory().createBookingData(); + var bookingInformation = BookingDataFactory.createBookingData(); - AccountPage accountPage = new AccountPage(); - accountPage.fillEmail(bookingInformation.getEmail()); - accountPage.fillPassword(bookingInformation.getPassword()); - accountPage.selectCountry(bookingInformation.getCountry()); - accountPage.selectBudget(bookingInformation.getDailyBudget()); + var accountPage = new AccountPage(); + accountPage.fillEmail(bookingInformation.email()); + accountPage.fillPassword(bookingInformation.password()); + accountPage.selectCountry(bookingInformation.country()); + accountPage.selectBudget(bookingInformation.dailyBudget()); accountPage.clickNewsletter(); accountPage.next(); - RoomPage roomPage = new RoomPage(); - roomPage.selectRoomType(bookingInformation.getRoomType()); + var roomPage = new RoomPage(); + roomPage.selectRoomType(bookingInformation.roomType().get()); roomPage.next(); - DetailPage detailPage = new DetailPage(); - detailPage.fillRoomDescription(bookingInformation.getRoomDescription()); + var detailPage = new DetailPage(); + detailPage.fillRoomDescription(bookingInformation.roomDescription()); detailPage.finish(); assertThat(detailPage.getAlertMessage()) diff --git a/src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener b/src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener new file mode 100644 index 0000000..dda40f5 --- /dev/null +++ b/src/test/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener @@ -0,0 +1 @@ +com.eliasnogueira.report.AllureTestLifecycleListener diff --git a/src/test/resources/allure.properties b/src/test/resources/allure.properties new file mode 100644 index 0000000..80b02dd --- /dev/null +++ b/src/test/resources/allure.properties @@ -0,0 +1 @@ +allure.results.directory=target/allure-results diff --git a/src/test/resources/general.properties b/src/test/resources/general.properties index 947dd63..15bb713 100644 --- a/src/test/resources/general.properties +++ b/src/test/resources/general.properties @@ -1,14 +1,17 @@ -# target execution: local or remote +# target execution: local, selenium-grid or testcontainers target = local +# browser to use for local and testcontainers execution +browser = chrome + # initial URL -url.base = http://eliasnogueira.com/external/selenium-java-architecture/ +url.base = https://eliasnogueira.com/external/selenium-java-architecture/ # global test timeout timeout = 3 -# javafaker locale -faker.locale = pt-BR +# datafaker locale +faker.locale = en-US # headless mode only for chrome or firefox and local execution -headless = false \ No newline at end of file +headless = false diff --git a/src/test/resources/local.properties b/src/test/resources/local.properties deleted file mode 100644 index 07d1868..0000000 --- a/src/test/resources/local.properties +++ /dev/null @@ -1 +0,0 @@ -browser = chrome \ No newline at end of file diff --git a/src/test/resources/grid.properties b/src/test/resources/selenium-grid.properties similarity index 70% rename from src/test/resources/grid.properties rename to src/test/resources/selenium-grid.properties index eb20844..90d2540 100644 --- a/src/test/resources/grid.properties +++ b/src/test/resources/selenium-grid.properties @@ -1,3 +1,3 @@ # grid url and port grid.url = localhost -grid.port = 4444 \ No newline at end of file +grid.port = 4444 diff --git a/src/test/resources/suites/local.xml b/src/test/resources/suites/local.xml index fa5f07a..089ee73 100644 --- a/src/test/resources/suites/local.xml +++ b/src/test/resources/suites/local.xml @@ -1,11 +1,11 @@ - + - + - \ No newline at end of file + diff --git a/src/test/resources/suites/parallel.xml b/src/test/resources/suites/selenium-grid.xml similarity index 98% rename from src/test/resources/suites/parallel.xml rename to src/test/resources/suites/selenium-grid.xml index cb1bd32..04d9024 100644 --- a/src/test/resources/suites/parallel.xml +++ b/src/test/resources/suites/selenium-grid.xml @@ -15,4 +15,4 @@ - \ No newline at end of file +