Chapter 13. Testing server-side rendering

This chapter covers

  • Understanding server-side rendering
  • Unit testing server-side rendered Vue components
  • Testing HTTP response codes from a server

Server-side rendering (SSR) is when you render the HTML code of an application on the server before returning it to the client. It’s common to use Vue for SSR, so to be a versatile Vue tester you should learn the techniques to test server-side rendered apps.

SSR is a big topic—too big to go over in a book on testing—so this chapter won’t teach you the specifics of how to add SSR to an app. Instead, I’ve converted the Hacker News app into an SSR app by following the official guide (https://vuejs.org/v2/guide/ssr.html). That way, you can focus on testing, rather than adding boilerplate.

Note

To follow along with this chapter, you need to switch to the chapter-13 branch of the Git project. If you’ve forgotten how to do that, you can go to appendix A for instructions. You’re going to use Node in this chapter to serve the application, so you’ll need an understanding of serving applications with Node.

Although I’m not going to teach the technical details involved in setting up a SSR app, I’ll give you a high-level overview of SSR. You don’t need to be familiar with SSR to follow along with this chapter and learn some useful skills.

The fundamental difference between testing SSR code and client-side code is that it runs on the server rather than in a browser. All the tests you’ve written so far have been for code that runs in a browser. That’s going to change in this section.

The first section of this chapter is about understanding SSR. The second part is about writing unit tests for server-side rendered components.

The final part is about testing the status codes returned from a server. When you add SSR to an app, you become responsible for the server and sending the correct HTTP codes to the user. You can test HTTP codes by writing an integration test, which is a kind of test you haven’t seen yet. I’ll show you how to test status codes returned by SSR apps. This section isn’t Vue specific and can be applied to any SSR app, but it’s a useful testing technique that I don’t often see covered.

The first thing to do is learn about what server-side rendering is and why it’s useful.

13.1. Understanding server-side rendering

With Vue SSR, a server generates the initial HTML of an application using Vue. When the code runs on the client, Vue hydrates the static HTML to make it interactive.

Note

Hydrating a server-side app is the process of making an app interactive by adding event listeners to the HTML. It’s much faster to hydrate an app, instead of rerendering the HTML client side. Vue hydrates a server-side rendered app automatically when mounting on the client side.

SSR offers some benefits, as well as some drawbacks. First let’s look at the benefits.

13.1.1. The advantages of SSR

SSR has the following two big advantages over client-side rendered apps:

  • Improved search engine optimization (SEO)
  • Improved time to content

A typical client-side rendered Vue app is an HTML page with a <div> and a script element, as shown in the next code. When the JavaScript is loaded and evaluated, the app is rendered into the <div>.

Listing 13.1. HTML response for a client-side app
<head>
...
</head>
<body>
  <div id="app" />                            1
  <script src="/dist/app.js"></script>        2
</body>

  • 1 Root div that the app will be rendered into
  • 2 Links to the script that will mount the Vue app and render it into the div

This works fine. But serving web pages like this can cause problems for search engines like Google and Bing. Search engines use crawler bots to index sites. The bots make a request to a web page and then index the response. Search engines then use the indexed response to decide whether a page contains relevant information to show to users, so it’s important that the response contains the rendered page content.

Search engines can execute JavaScript on a page before they save the HTML, but it doesn’t always work. For example, if data is fetched in an asynchronous call, the search engines might index the site before the asynchronous call has finished and the data has rendered.

If a site isn’t fully rendered when a crawler indexes it, the site’s page ranking, otherwise known as SEO, will be negatively affected. For most websites, SEO is vital, so any chance that client-side rendered content will harm SEO is avoided. Server-side rendering ensures that search engines correctly index the page content, because it’s returned as static HTML from the server.

The other benefit of SSR is that it improves the time to content. When you request a web page with client-side rendering, the page is empty until the JavaScript downloads and executes (figure 13.1). On a slow connection, this can take a long time!

Figure 13.1. The process of a client-side rendered app

A SSR response, shown in the next listing, would contain all the HTML of an app, so the browser can render content as soon as it has parsed the HTML (figure 13.2).

Figure 13.2. The process of a server-side rendered app

Listing 13.2. HTML response for a server-side app
<head>
...
</head>
<body>
  <div id="app">
    <header class="header">
      ...
    </header>
    <div class="view">
      ...
    </div>
  </div>
  <script src="/dist/app.js"></script>       1
</body>

  • 1 The script that hydrates the Vue application on the client side

So the benefits of SSR are improved SEO and improved time to content. But there are also some downsides to SSR.

13.1.2. The disadvantages of SSR

SSR isn’t all rainbows and kittens. It has some serious downsides:

  • SSR makes your code more complex.
  • SSR requires a server.
  • SSR increases the server-side load.

The first downside is that it makes the code more complex. When you write code to run on both the client side and server side, you need to consider which environment the code will run in. For example, on the client side, document is a global object. You can set the title of the page with document.title. You can’t do this on the server side, because in Node there is no document object. If you try to access document.title in your application code—for example, in a mixin—Node will create an error, and the application won’t be served.

The second downside is that SSR requires a server. For a normal Vue.js app, all the content could be served as part of a static site. With server-side rendering, you need to manage your own server to render the application. This makes hosting and deploying an application more complex and probably more expensive.

Definition

A static site is a site that contains only static assets, like HTML files, JavaScript files, and CSS files. You can use services to host static websites on your behalf, which means you don’t need to manage a server.

If you already use a server to serve your site, you can update the server-side code to render the application before it serves it. Unfortunately, rendering on the server uses more CPU than sending back a static asset, which will increase the server response time. You need to make sure you cache your pages effectively so you don’t increase memory consumption too much.

Note

You can create the server using any server-side language that supports a JavaScript sandbox. That means you can write your server in PHP, Ruby, or Python.

Now that you know the advantages and disadvantages of SSR, it’s time to take a look at your application and how you can test it.

13.2. Testing server-side rendered components

Unit testing server-side rendered components presents some challenges, but the techniques are pretty simple.

Note

I’m not going to teach you the steps for adding SSR to a project, but I’ve updated the Hacker News app to use SSR; you can find it in the chapter-13 branch. It follows the conventions that are laid out in the Vue SSR docs. If you want a good understanding of SSR, I recommend reading those docs—https://ssr.vuejs.org/en.

You can’t use Vue Test Utils to test server-side rendered components, because Vue Test Utils needs to be run in a browser environment. The way Vue Test Utils works under the hood is that it mounts a component, which creates DOM nodes. Server-side rendered code is different: it returns a string of HTML without creating DOM nodes.

To create a string of HTML from a Vue instance, you use the vue-server-renderer package. The renderToString method returns a string that contains the HTML generated from a Vue instance, as shown in the next listing.

Listing 13.3. Rendering a Vue instance to a string
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()                          1
const vm = new Vue({                                       2
  template: '<div />'
})
renderer.renderToString(vm)                                3

  • 1 Creates a renderer
  • 2 Creates an instance
  • 3 Renders an instance to a string

When you write unit tests for server-side rendered code, you do something similar. You can generate a string and then assert that the string is correct.

Throughout this book, I’ve discussed how you should provide an input in a test and assert against an output. The output of server-side rendered components is almost always a string, so the technique to test these components is relatively straightforward: You provide a component with the correct input and then assert that the string generated from the component either contains the correct HTML or matches a snapshot.

I’m going to be honest: I don’t write many unit tests for server-side rendered components. I write unit tests for the components using the client-side Vue Test Utils. That way, I can interact with the component instance. I rely on end-to-end tests to catch problems with server-side rendered components. But I know that other developers do unit test server-side rendered components. Some large companies even unit test every single component on both the server and the browser, so it’s a valid test approach.

Two compelling reasons to write unit tests for server-side rendered components follow:

  • The component behaves differently on the browser than the server.
  • You only rely on unit tests and snapshot tests (no end-to-end tests).

To test server-side rendered components, you can use the Vue Server Test Utils library.

13.2.1. Using Vue Server Test Utils

Vue Server Test Utils is a testing library for server-side rendered Vue components. It shares the same mounting API as Vue Test Utils and accepts the same options to create a component instance, but instead of returning a wrapper object containing a mounted instance, Vue Server Test Utils returns a string of rendered HTML (see listing 13.4).

Note

Under the hood, Vue Server Test Utils uses the vue-server-renderer package. You can read about the API in the docs—https://ssr.vuejs.org/en/api.html.

Listing 13.4. Writing a snapshot test with renderToString
import { renderToString } from '@vue/server-test-utils'
import Component from './Component.vue'

test('renders correctly on server ', () => {
  const str = renderToString(Component, {           1
    propsData: { msg: 'Hello, World!' }
  })
  expect(str).toContain('<p>Hello, World!</p>')     2
})

  • 1 Creates a string using the Vue Server Test Utils renderToString method
  • 2 Asserts that the rendered string contains a <p> element with the text “Hello, World!”

To practice writing tests for server-side rendered components, you’re going to write a snapshot test for a NotFound view component. The NotFound component is rendered when Vue Router can’t find a matching route.

Note

The Hacker News app with SSR uses Vue Router on the server and the client.

First, add Vue Server Test Utils as a dev dependency to the project:

npm install --save-dev @vue/server-test-utils

Create a file in src/views/__tests__/NotFound.server.spec.js. The .server file extension helps differentiate between client-side tests and server-side tests.

To create a snapshot test, generate the HTML string of the page, and pass it to the Jest toMatchSnapshot matcher. Add the code from the next listing to src/views/__tests__/NotFound.server.spec.js.

Listing 13.5. Writing a snapshot test with renderToString
import { renderToString } from '@vue/server-test-utils'
import NotFound from '../NotFound.vue'

describe('NotFound', () => {
  test('renders correctly on server ', () => {
    const str = renderToString(NotFound)           1
    expect(str).toMatchSnapshot()
  })
})

  • 1 Renders the NotFound component to a string with Vue Server Test Utils

Before you run the tests, add a beforeCreate method to the NotFound component. Then add the following code to the options object inside the <script> block of src/views/NotFound.vue:

beforeCreate() {
  document.title = 'hello'
}

Now run the unit tests with the npm run test:unit command. The test will pass, and the snapshot test will be written. The thing is, this test shouldn’t pass. Build and start the server as follows:

npm run build && npm run start

Now navigate to localhost:8080/does-not-exist. You’ll see that the application is broken in production and returns a 500 error. The reason it’s broken is that you’re trying to set document.title when you render the code on the server, but document is undefined when you render the component in Node. Makes sense, but why is the unit test passing?

Note

If you get an EADDRINUSE error, that means a process is already listening to port 8080. You need to stop the process from listening to 8080, or add a PORT environment variable in front of the start command: npx cross-env PORT=1234 npm run start.

The problem is that Jest runs tests inside a jsdom environment by default, which means DOM properties like document are defined when you run the test. This can lead to false positives where your unit tests pass but the code fails in production when you render the component on the server.

When you write unit tests for server-side rendered components, you should run the tests in a Node environment, not a jsdom environment. In Jest, you can set this in the test file, by adding a comment to the top of the file. Add the following code to the top of src/views/__tests__/NotFound.server.spec.js.

Listing 13.6. Setting a Jest test file to run in a Node environment
/**
 * @jest-environment node
 */

Run the tests again. You should see a warning that document is undefined, and the test will fail because the snapshots don’t match. Perfect—now your tests are running in a realistic environment, and the tests fail correctly. You can remove the before-Create hook and run the tests again to make sure they’re passing.

You’ve just seen the problem with running tests for server-side rendered components in a jsdom environment. This is another reason that you should split server-side tests and client tests into separate files. You can run each test file in only one environment. I use the convention of adding a .server extension to identify test files for server-side rendered code. A server-side test file for ItemList would be ItemList.server .spec.js, for example, and a client-side test file would be ItemList.spec.js.

Note

When you use Vue Server Test Utils, VUE_ENV is set to server. This is fine when you separate tests into client-side and server-side tests and use Jest, because Jest runs each test in a new process. But this setup could be a big problem if you use other tests runners that don’t run test files in separate processes. If you use a different test runner, you should use one script to run tests for client-side code and another script to run tests for server-side code.

The renderToString method you used returns a string of HTML. But what if you want to traverse the server-side rendered markup? Vue Server Test Utils also exports a method to traverse the rendered output—render.

13.2.2. Traversing server-side rendered markup with render

Vue Server Test Utils exposes two methods—renderToString and render. renderToString returns a string of the rendered markup. You can run only a few assertions on a string. If you want to traverse the server-side rendered markup, you should use the render method.

The Vue Server Test Utils render method returns a wrapper object similar to the normal Vue Test Utils wrapper object, which I’m sure you’re familiar with now. The Vue Server Test Utils wrapper has methods to traverse and assert the rendered string, but the API is slightly different from the Vue Test Utils wrapper. The difference is that the render method returns a Cheerio wrapper object. Cheerio is a Node implementation of the jQuery API, so it should feel familiar if you’ve used jQuery before. If not, you can find extensive docs at https://cheerio.js.org.

Using the render method with the Cheerio API can make your test code easier to read, and you can write assertions that are difficult to write when you’re just using the renderToString method. Add render to the import statement in src/views/__tests__/NotFound.server .spec.js as follows:

import { renderToString, render } from '@vue/server-test-utils'

You’ll add a new test to the NotFound component that uses the render method. The test will check that NotFound renders an <h1> element that contains the correct text by calling find on the wrapper with an h1 selector and calling a text method to return the text of the <h1> element.

Add the code from the following listing to the describe block in src/views/__tests__/NotFound.spec.js. This test will pass, because the code already exists.

Listing 13.7. Using render to assert against server-side rendered code
test('renders 404 inside <h1> tag', () => {
  const wrapper = render(NotFound)                    1
  expect(wrapper.find('h1').text()).toBe('404')       2
})

  • 1 Generates a Cheerio wrapper
  • 2 Finds an <h1> tag, and asserts that it contains the text “404”

You can check that the tests pass with npm run test:unit. If you want to be extra careful, you should edit the 404 page to make sure it fails for the correct reason.

Note

To see the full list of methods available for the Cheerio wrapper, visit the Cheerio docs—https://cheerio.js.org.

That’s how you can unit test server-side rendered components with Vue Server Test Utils. Server-side rendered component unit tests are normally simple, because you’re not asserting against a running instance, but you can use the same techniques that you use when you write normal unit tests. The main difference is that SSR tests need to run in a node environment using the Vue Server Test Utils library.

One other useful technique for testing SSR is to check that a server responds with the correct status codes. You can write tests to check that using SuperTest.

13.3. Testing status codes with SuperTest

When you add SSR to your app, you’re responsible for responding to requests with the correct HTTP status codes. Normally, there’s some logic involved in how you respond with status codes, so it can be beneficial to write tests to check that you’re responding with them correctly.

You can test that your server responds with the correct status codes using SuperTest. SuperTest is a library used to test HTTP responses.

Note

In this section, you’re going to test HTTP responses. I’ll talk about the response body and the response headers. If you aren’t familiar with response bodies, response headers, or HTTP, read MDN’s excellent primer on HTTP: http://mng.bz/0WWW. If you’re familiar with HTTP but need to refresh your knowledge of HTTP responses, read this MDN page on HTTP responses: http://mng.bz/K11E.

This process is a departure from the tests you’ve written so far. The focus of this book is on frontend tests—unit tests, end-to-end tests, and snapshot tests. The tests I’m about to teach you are integration tests.

Integration tests are tricky to define. Different people have different definitions, and there isn’t a definitive answer. Roughly, though, integration tests examine parts of an application that work together, but they don’t test the entire system.

For example, the tests I’m going to teach you will make a request to a server, and check that the server responds with the correct status code. They don’t test the client-side code, just that the server response is correct.

First, you should write a basic sanity test that makes a request to facebook.com to be sure the setup works correctly. Install SuperTest as a dev dependency by running the following command:

npm install --save-dev supertest

Now create a server.spec.js file in the root of the project. You’ll add all your SuperTest tests in this file. Add the code from the next listing into server.spec.js to be a request to facebook.com and assert that it responds with a 200.

Listing 13.8. Testing an HTTP request with SuperTest
import request from 'supertest'                  1

test('returns 200', () => {
  return request('https://www.facebook.com')     2
    .get('/')                                    3
    .expect(200)                                 4
})

  • 1 Imports SuperTest as a request; this is the convention for importing SuperTest.
  • 2 Because SuperTest is asynchronous, you need to return the promise that it returns.
  • 3 Makes a get request to the root of the base URL
  • 4 Asserts that the HTTP status code is a 200

Run the unit tests with npm run test:unit. Jest will pick up server.spec.js and run the new test. The test you added should pass, although it might fail if you’re in a country where Facebook forces redirects. Don’t worry—a test that fails because Facebook returned a 302 rather than a 200 still proves that SuperTest is set up correctly.

You might have noticed that the tests take slightly longer to run now. Tests that use SuperTest take longer than normal unit tests because they make HTTP requests. If you have hundreds of tests using SuperTest, your test suite could take minutes to run. Minutes are years in unit test time, and you shouldn’t slow down your unit tests. Instead, you should separate unit tests and integration tests to make sure your unit tests are fast.

To do that, open your package.json file. Edit the test:unit script to run tests only in the src directory as follows:

  "test:unit": "jest src --no-cache"
Note

Here the –-no-cache flag is added to fix bugs with older versions of Windows. If you’re using macOS or Linux, you can omit the no-cache flag.

Now create a new test:integration script that runs the SuperTest tests. Because SuperTest creates a running app using the client-side JavaScript bundle, you need to create a new bundle for the server to use each time you run the tests. If you don’t create a new bundle, you’ll be testing old code. To build the bundle, you can run the build script before you run the tests. Unfortunately, this slows down the tests even more!

SuperTest tests need to run in a Node environment, like the server-side rendered unit tests. In your package.json file, add to the scripts field a new test:integration script that runs the server.spec.js file with Jest using a Node env:

"test:integration": "npm run build && jest --testEnvironment node --forceExit server.spec.js"

Add the integration script to the test script in the package.json file like so:

"test ": "npm run lint && npm run test:unit && npm run test:integration"

Run the integration tests with the command npm run test:integration. This is how you’ll run the tests that use SuperTest.

You’ve seen that you can use SuperTest to request a URL and assert that the response from the URL was correct. You don’t want to assert against Facebook, though; you want to check that your own app works correctly. To do that, set the server running in your tests. It’s easy to create a running server with SuperTest—import the application express app, and pass it in to SuperTest. Refactor server.spec.js to include the code in the next listing.

Listing 13.9. Testing a 200 response with SuperTest
import app from './server'

describe('server', () => {
  test('/top returns 200', () => {
    return request(app)                 1
      .get('/top')                      2
      .expect(200)                      3
  })
})

  • 1 Creates the server by passing the app to SuperTest
  • 2 Makes a get request to the /top route
  • 3 Asserts that the server responds with a 200

Checking 200 responses is great, but you can implicitly test 200s with end-to-end tests (which you’ll write in the next chapter). Instead of testing 200 errors, let’s write some tests for a 404-error response.

To check that you get a 404-error response, you can make a GET request to a route that you know doesn’t exist. Copy the code from the following listing into server.spec.js.

Listing 13.10. Testing that the server responds with 404 when the page isn’t found
test('returns a 404 when page does not exist', () => {
  return request(app)
    .get('/does-not-exist')          1
    .expect(404)                     2
})

  • 1 Makes a GET request to a route that doesn’t exist
  • 2 Asserts that the server responds with a 404 error

This test will pass; the app already handles 404-error responses. You can change the HTTPStatusCode property in src/views/NotFound.vue to make sure the test fails with an assertion error.

Note

The project uses a mixin that uses the HTTPStatusCode property to set the status code. This isn’t a standard Vue feature!

I recommend having one SuperTest for each error-status code the server should return, normally a 200, a 404, and a 500. The main benefit of SuperTest is how easy it is to test the server response, so you can check that you have the correct HTTP headers, like cache-control or link.

You should avoid testing markup in SuperTest tests. When you have lots of tests that use SuperTest, they take a long time to run. You can test the markup of components in your unit tests or snapshot tests instead.

The main reason I use SuperTest is to check HTTP status codes. With other end-to-end tests, it’s not always possible to determine whether the code you’re testing was returned by the server as part of the HTTP request or whether it was rendered by the script executing after the server returned the code. With SuperTest, you can test explicitly that the server-side rendered code has been included in the HTTP response.

Now that you’ve seen how to write unit tests and integration tests for server-side tests, I want to cover testing SSR implicitly and why it’s often more valuable than explicitly unit testing server-side rendered code.

13.4. Testing SSR implicitly

I’ve written in this book about testing implicitly and testing explicitly. Explicit testing is writing tests that assert that some functionality is working correctly. Writing unit tests for SSR code is explicit testing. Implicit testing is testing functionality as part of another test by relying on that functionality for the test to pass. If you test some functionality implicitly, the test will break if the functionality isn’t working correctly, but the test isn’t asserting it directly.

For example, end-to-end tests are tests that start up an application and run through user journeys. These tests do a lot of implicit testing, as well as checking that all the units of your application work together correctly.

It’s useful to have a mixture of implicit and explicit tests. Implicit tests are good because you can test more functionality with fewer tests. The downside is that when an implicit test fails, it’s difficult to pinpoint which part of the code is causing the problem. There’s no explicit assertion that says, “Hey, here’s what’s wrong with your code”; you’ll have to debug the stack trace to see what went wrong. The benefit of testing code implicitly is that you don’t need to spend time writing tests, and you can refactor the code easily.

You won’t explicitly test a lot of the code used to set up the server-side rendering. For example, in the server-side Hacker News app you have two entry files, one for the server and one for the route. You shouldn’t write unit tests that check that the entry files are set up correctly. The files will be tested implicitly by end-to-end tests that load a page and run through a user journey. If the configuration is incorrect, the end-to-end tests won’t be able to run through the user journeys, and the tests will fail.

Similarly, a lot of SSR code can be implicitly tested by end-to-end tests, without writing separate unit tests. Often, with SSR code, the cost of writing explicit unit tests outweighs the benefits. But there are cases where writing a unit test for SSR code is beneficial. I’ll leave that to your discretion.

In the next and final chapter, you’ll learn how to write end-to-end tests to finish off your test suite.

Summary

  • You can test server-side rendered code with the Vue Server Test Utils library.
  • You can traverse the string output of a server-side rendered component with the Vue Server Test Utils render method.
  • You should write server-side unit tests in a separate file from client-side unit tests, so that you can run them in a Node environment, instead of a jsdom environment.
  • You can test HTTP status codes with the SuperTest library.

Exercises

1

What comment can you use to make Jest run tests in a Node environment?

2

What is the difference between render and renderToString?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.160.61