├── .gitignore ├── img ├── diff1-2.png ├── diff1-3.png ├── version1.png ├── version2.png ├── version3.png └── testPyramid.png ├── sample_page └── index.html ├── src ├── screenshot.js ├── pixeldiff.js └── login.js ├── __tests__ ├── analytics.test.js ├── desktop.test.js ├── mobile.test.js ├── login.test.js ├── form.test.js └── seo.test.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .history -------------------------------------------------------------------------------- /img/diff1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/puppeteer-investigation/master/img/diff1-2.png -------------------------------------------------------------------------------- /img/diff1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/puppeteer-investigation/master/img/diff1-3.png -------------------------------------------------------------------------------- /img/version1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/puppeteer-investigation/master/img/version1.png -------------------------------------------------------------------------------- /img/version2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/puppeteer-investigation/master/img/version2.png -------------------------------------------------------------------------------- /img/version3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/puppeteer-investigation/master/img/version3.png -------------------------------------------------------------------------------- /img/testPyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilhan-mstf/puppeteer-investigation/master/img/testPyramid.png -------------------------------------------------------------------------------- /sample_page/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 8 | 9 | 10 |28 | 29 | As this blog post is related to automated tests with Puppeteer, it can be used both end-to-end and user interface tests. Even if end-to-end tests are triggered from user interface, end-to-end tests covers whole flow and it doesn't have to check all possible cases on the UI. 30 | 31 | 34 | 35 | ### When do you really need end-to-end testing? 36 | End-to-end tests are useful to identify problems in user journeys. User journeys are the flows that a user can follow in your application. Therefore, the idea of end-to-end tests is to imitate a user's behavior in certain flows from start to end to ensure that everything works as expected. Running end-to-end tests are slower than to unit tests since they can touch many components of your application or third party services. As a result, we can think of them as expensive in terms of resources that reserved for them. For instance, assume that you want to run end-to-end tests of a web application, to accomplish this you need to run a browser and this browser consumes significant amount of memory. Furthermore, the initialization of the browser and the other components takes a lot of time. Hence, only relying on end-to-end tests is not preferable in terms of development efficiency. Therefore, you may want to write end-to-end tests for high-value interactions of your application such as checkout, login, etc. 37 | 38 | ## What is Puppeteer? 39 | [Puppeteer](https://pptr.dev/) provides a high-level API to control Chrome or Chromium programmatically. It is an open-source Nodejs library. Puppeteer runs headless (i.e. a browser that doesn't have a user interface) by default, but it can be configured to run full (non-headless) Chrome or Chromium. 40 | 41 | With this tool, you can run your web application on a browser and imitate user actions programmatically. 42 | 43 | ### Use cases: 44 | - Generate screenshots and PDFs of pages. 45 | - Crawl an SPA (Single-Page Application) and generate pre-rendered content (i.e. "SSR" (Server-Side Rendering)). 46 | - Automate form submission, UI testing, keyboard input, etc. 47 | - Create an up-to-date, automated testing environment. Run your tests directly in the latest version of Chrome using the latest JavaScript and browser features. Run regression and end-to-end tests. 48 | - Capture a timeline trace of your site to help diagnose performance issues. 49 | - Test Chrome Extensions. 50 | - Check for console logs and exceptions. 51 | - Replicate user activity. 52 | 53 | ### Jest and Puppeteer 54 | Puppeteer API is not designed for testing and it doesn't provide you the whole functionality of a testing framework. Therefore, it can be used with [Jest](https://jestjs.io/) JavaScript testing framework. The samples on this blog post use [jest-puppeteer](https://www.npmjs.com/package/jest-puppeteer) Nodejs library. It provides all required configuration for writing integration tests using Puppeteer with Jest. 55 | 56 | ### Samples 57 | This section provides a couple of examples to give you better insights into Puppeteer's usage. They don't cover all the features of Puppeteer and this blog post doesn't aim to give you detailed information of the Puppeteer API. You can build upon the given examples and explained concepts. 58 | 59 | The shown examples are also available at this [GitHub repository](https://github.com/ilhan-mstf/puppeteer-investigation). 60 | 61 | #### Prerequistes and Installation 62 | You need: 63 | - The recent stable version of node.js 64 | - The recent stable version of yarn or npm 65 | 66 | Examples have these dependencies: 67 | 68 | ```json 69 | "devDependencies": { 70 | "jest": "^25.1.0", 71 | "jest-puppeteer": "^4.4.0", 72 | "pixelmatch": "^5.1.0", 73 | "puppeteer": "^2.1.1" 74 | } 75 | ``` 76 | 77 | `puppeteer` library downloads lastest Chrome executable. You can use `puppeteer-core` instead of `puppeteer` if you want to use existing Chrome executable in your system and pass the path of executable as a configuration option. 78 | 79 | You can run the tests with `yarn test` command. 80 | 81 | #### Taking Screenshot 82 | You can take screenshot of your website with different options such as setting viewport or emulated device. 83 | 84 | ```js 85 | const puppeteer = require('puppeteer'); 86 | 87 | (async () => { 88 | const browser = await puppeteer.launch(); 89 | const page = await browser.newPage(); 90 | await page.goto('https://designisdead.com/'); 91 | await page.screenshot({path: 'homepage.png'}); 92 | 93 | await browser.close(); 94 | })(); 95 | ``` 96 | 97 | #### Comparing Screenshots 98 | You can detect changes visually by taking screenshots of different versions of your application. You can take screenshots with Puppeteer but you need another tool to compare them. This sample uses [`pixelmatch` library](https://www.npmjs.com/package/pixelmatch). 99 | 100 | ```js 101 | const fs = require('fs'); 102 | const PNG = require('pngjs').PNG; 103 | const pixelmatch = require('pixelmatch'); 104 | 105 | const img1 = PNG.sync.read(fs.readFileSync('version1.png')); 106 | const img2 = PNG.sync.read(fs.readFileSync('version2.png')); 107 | const {width, height} = img1; 108 | const diff = new PNG({width, height}); 109 | 110 | pixelmatch(img1.data, img2.data, diff.data, width, height, {threshold: 0.9}); 111 | 112 | fs.writeFileSync('diff.png', PNG.sync.write(diff)); 113 | ``` 114 | 115 | 126 | 127 | |  |  |  | 128 | |:--:|:--:|:--:| 129 | | Version 1 | Version 2 | Diff 1-2 | 130 | 131 | |  |  |  | 132 | |:--:|:--:|:--:| 133 | | Version 1 | Version 3 | Diff 1-3 | 134 | 135 | Be aware that some of image comparision tools find differences by checking the pixel difference, therefore, if the text is changed, they will show it as a change. In other words, if you change your content, you will see it as a difference. 136 | 137 | 145 | 146 | #### Checking integration of third party applications 147 | You may use third party services, scripts, etc. in your application. Therefore, it will be a good idea to check that their integration with your application works as expected. 148 | 149 | ```js 150 | describe('Analytics', () => { 151 | beforeAll(async () => { 152 | await page.goto('https://designisdead.com/') 153 | }) 154 | 155 | it('should return google tag manager', async () => { 156 | const tagManager = await page.evaluate(() => google_tag_manager) 157 | expect(tagManager).toBeDefined() 158 | }) 159 | }) 160 | ``` 161 | 162 | #### Mobile and Desktop Layout 163 | Since there is a significant variance in screen sizes, there are a lot of cases that need to be tested here. 164 | 165 | ```js 166 | const devices = require('puppeteer/DeviceDescriptors'); 167 | const iPhonex = devices['iPhone X']; 168 | 169 | describe('Mobile', () => { 170 | beforeAll(async () => { 171 | await page.emulate(iPhonex) 172 | await page.goto('https://designisdead.com/') 173 | }) 174 | 175 | it('should render hamburger menu', async () => { 176 | await page.waitForSelector('.Page-hamburger', { 177 | visible: true 178 | }) 179 | }) 180 | }) 181 | ``` 182 | 183 | ```js 184 | describe('Desktop', () => { 185 | beforeAll(async () => { 186 | await page.setViewport({ width: 1280, height: 768 }) 187 | await page.goto('https://designisdead.com/') 188 | }) 189 | 190 | it('should not render hamburger menu', async () => { 191 | await page.waitForSelector('.Page-hamburger', { 192 | hidden: true 193 | }) 194 | }) 195 | }) 196 | ``` 197 | 198 | #### Seo checks 199 | Since search engines crawl your production website, it may be a good idea to check your pages' SEO performance. However, even if the below example handles this issue on test cases, you may want to generate a score and corresponding report. 200 | 201 | ```js 202 | describe('SEO', () => { 203 | beforeAll(async () => { 204 | await page.goto('https://designisdead.com/') 205 | }) 206 | 207 | it('should display "Design is Dead" text on title', async () => { 208 | await expect(page.title()).resolves.toMatch('Design is Dead') 209 | }) 210 | 211 | it('should have description meta-tag', async () => { 212 | const descriptionContent = await page.$eval("head > meta[name='description']", element => element.content); 213 | 214 | expect(descriptionContent).toBeDefined(); 215 | }) 216 | 217 | it('should have a headline', async () => { 218 | const headlines = await page.$$('h1') 219 | 220 | expect(headlines.length).toBe(1) 221 | }) 222 | }) 223 | ``` 224 | 225 | #### Login 226 | One of the significant features of web applications is to log in user's account. It can be count as an example of End-to-End tests since back-end services involve to the process. 227 | 228 | ```js 229 | // put GITHUB_USER and GITHUB_PWD values to .env file 230 | require('dotenv').config() 231 | 232 | describe('Github - Login', () => { 233 | beforeAll(async () => { 234 | await page.goto('https://github.com/login') 235 | }) 236 | 237 | it('should log in and redirect', async () => { 238 | await page.type('#login_field', process.env.GITHUB_USER) 239 | await page.type('#password', process.env.GITHUB_PWD) 240 | await page.click('[name="commit"]', {waitUntil: 'domcontentloaded'}) 241 | const uname = await page.$eval('#account-switcher-left > summary > span.css-truncate.css-truncate-target.ml-1', e => e.innerText) 242 | 243 | await expect(uname).toMatch(process.env.GITHUB_USER) 244 | }) 245 | }) 246 | ``` 247 | 248 | ## Selenium and other tools to automate browsers 249 | Puppeteer is not the only tool that provides higher level API to manage and automate browsers. [Playwright](https://www.npmjs.com/package/playwright) is an alternative Node library that supports Chromium, Firefox and WebKit. It is developed by the same team built Puppeteer and its API is very similar to Puppeteer. Another options is [Selenium](https://www.selenium.dev/). It supports all the major browsers. Futher, you can use Selenium with Java, Python, Ruby, C#, JavaScript, Perl and PHP. 250 | 251 | ## Conclusion 252 | In this blog, we go over the levels of software testing and discuss the use cases of end-to-end tests. Later, we look into the details of Puppeteer as an end-to-end test tool by giving some examples. 253 | 254 | ## References: 255 | - [1] https://martinfowler.com/articles/practical-test-pyramid.html 256 | - [2] Mike Cohn, Succeeding with Agile 257 | 258 | ## Suggested Readings: 259 | - https://martinfowler.com/bliki/TestPyramid.html 260 | - https://kentcdodds.com/blog/write-tests 261 | - https://www.freecodecamp.org/news/why-end-to-end-testing-is-important-for-your-team-cb7eb0ec1504/ 262 | - https://www.symphonious.net/2015/04/30/making-end-to-end-tests-work/ 263 | - https://blogs.dropbox.com/tech/2019/05/athena-our-automated-build-health-management-system/ 264 | - https://medium.com/coursera-engineering/improving-end-to-end-testing-at-coursera-using-puppeteer-and-jest-5f1bac9cd176 265 | - Puppeteer example scripts: https://github.com/checkly/puppeteer-examples 266 | - Running Puppeteer on serverless: https://github.com/alixaxel/chrome-aws-lambda 267 | - https://medium.com/@ymcatar/visualization-on-steroid-using-headless-browser-to-auto-refresh-google-data-studio-dashboards-c195e68f10b 268 | - https://www.lambdatest.com/blog/why-selenium-automation-testing-in-production-is-pivotal-for-your-next-release/ 269 | 270 | ## Acknowledgements 271 | Thanks to Kris Barnhoorn and Manon Erb for sharing their valuable ideas and checking draft version of this blog post. 272 | 273 | -- Mustafa İlhan, 2020, İzmir 274 | 275 | 276 | --------------------------------------------------------------------------------expect(umbrellaOpens).toBe(true)
— Erin 🐠 (@erinfranmc) July 10, 2019
tests: 1 passed, 1 total
**all tests passed** pic.twitter.com/p6IKO7KDuy