Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test Renderer for TDD #515

Open
aciccarello opened this issue Sep 6, 2019 · 2 comments
Open

Test Renderer for TDD #515

aciccarello opened this issue Sep 6, 2019 · 2 comments
Labels
area: testing Testing enhancement New feature or request next Issue/Pull Request for the next major version

Comments

@aciccarello
Copy link
Contributor

Enhancement
I would like to have a way to render from tests as a way to aid with TDD. Ideally, you would be able to pass an assertion template or component instance to a helper function withing a test file and it would serve that component in a separate window. This would be helpful because you would be able to play with a component in isolation. You could adjust the css and dom structure of an assertion template before updating the component implementation.

Challenges

  • Build pipeline in tests which includes styles
  • Making this work with different test runners
  • Tests are shallow while rendering would be deep, might need different setup
  • Assertion templates currently don't care about functions in properties but rendering would
@matt-gadd matt-gadd added area: testing Testing enhancement New feature or request next Issue/Pull Request for the next major version labels Sep 10, 2019
@aciccarello
Copy link
Contributor Author

I have looked at this a bit more and see two divergent ways of approaching this given the following use case:

WHEN a developer is writing a component test but isn't sure how it should look
THEN they would use a utility to allow viewing the assertion template and/or harness render in a browser.

Ideally, this utility would be hooked into the test runner so that it would work with the user's testing workflow. However this is difficult because it involves rendering in a browser and there are a variety of strategies to running tests, many of which (intern-node and jest in particular) don't involve a real browser. Thus this, dojo test approach could require different code for each test runner/environment.

The alternate approach is to hook into @dojo/cli-build-app. The advantage to this is that it is already set up for compiling, serving, and rendering components. Since the tests deal with render functions, it is relatively simple to take something like an assertion template and render it in the browser, with styles, using the same approach to rendering an entire application.

Example of rendering an assertion template in main.tsx

// Can't have existing app render active
// const r = renderer(() => w(AppContainer, {}));
// r.mount({ registry });

const baseAssertion = assertionTemplate(() => (
	<div {...{ '~key': 'root' }} classes={css.root}>
		<h2 classes={css.title}>
			Filter
			<Button
				key="onclose"
				borderless
				classes={{ 'demo/Button': { root: [css.closeLink] } }}
				onClick={() => {
					console.log('onClick called');
				}}
			>
				<Icon icon="close" />
			</Button>
		</h2>
	</div>
));

const t = renderer(baseAssertion.append('~root', ['test']) as () => WNode);
t.mount({ registry });

The problem is then, how do you fit into the user testing workflow. If you tried to import an assertion template or test harness from a unit test you again need to do something about the test runner. This would most likely be some form of mocking but it can get complicated pretty quickly.

Example of test file that would need to be mocked out.

import assertionTemplate from '@dojo/framework/testing/assertionTemplate';
import harness from '@dojo/framework/testing/harness';
import tddRenderUtil from '@dojo/framework/testing/tddRenderUtil`';
import { tsx } from '@dojo/framework/widget-core/tsx';
import Button from '../button/Button';
import Icon from '../icon/Icon';
import { RightMenu } from './RightMenu';
import * as css from './RightMenu.m.css';
import { myUtilThatGetsCalled } from './util';
import { mockFunction } from './testing-utils' // This could mean more mocking needed

// Would need to run these (test runner specific) describe/beforeEach/it blocks
describe('RightMenu', () => {
	const baseAssertion = assertionTemplate(() => (
		<div {...{ '~key': 'root' }} classes={css.root}>
			<h2 classes={css.title}>
				Filter
				<Button
					classes={{ 'demo/Button': { root: [css.closeLink] } }}
					onClick={() => { console.log('onClick called'); }}
				>
					<Icon icon="close" />
				</Button>
			</h2>
		</div>
	));

	let someSpy;
	beforeEach(() => {
		someSpy = spyOn(myUtilThatGetsCalled).andStub(() => {custom: 'return obj'});
	});

	it('default renders correctly', () => {
		const h = harness(() => <RightMenu open={true} onClose={() => null} />);

		tddRenderUtil(h.getRender()); // Does this block?
		h.expect(baseAssertion);
	});

	it('alows changing the title', () => {
		const h = harness(() => <RightMenu title="Custom Title" open={true} onClose={() => null} />);

		tddRenderUtil( // Can you call this again?
			baseAssertion.replace('h2', [
				'Custom Title',
				...baseAssertion.getChildren('h2').slice(1)
			]) // Note: this is copying the existing code already
		);
		h.expect(
			baseAssertion.replace('h2', [
				'Custom Title',
				...baseAssertion.getChildren('h2').slice(1)
			])
		);
	});

	it('calls the close callback', () => {
		// We may not care about this test but it would need to be mocked correctly
		const onClose = mockFunction();
		const h = harness(() => <RightMenu open={true} onClose={onClose} />);

		h.trigger('@onclose', 'onClick');

		expect(onClose).toHaveBeenCalled();
	});
});

To try to import test files gets back into the territory of dealing with different test runners, even if we entirely mock them out. It would be a lot of work put on the user and extra complexity.

Considering that this is a tool for temporary test debugging, it might make more sense to have the user copy/paste the relevant code into a file specifically for the test render. Any test setup would need to be copied, but that would actually simplify the experience.

Example of render file separate from tests

// Could have own relevant helpers
export class SideBySide extends WidgetBase {
  protected render() {
    return (
      <div style="display: flex; flex-direction: row">
        <div style="width: 50%">{this.properties.expected()}</div>
        <div style="width: 50%">{this.properties.harness.getRender()}</div>
      </div>
    );
  }
}

const baseAssertion = assertionTemplate(() => (
  <div {...{ "~key": "root" }} classes={css.root}>
    <h2 classes={css.title}>
      Filter
      <Button
        classes={{ "demo/Button": { root: [css.closeLink] } }}
        onClick={() => {
          console.log("onClick called");
        }}
      >
        <Icon icon="close" />
      </Button>
    </h2>
  </div>
));

// User can work here without messing up tests
const h = harness(() => <RightMenu open={true} onClose={() => null} />);

tddRenderUtil(() => <SideBySide expected={baseAssertion} harness={h} />);

Technically, this is entirely available with Dojo, however it might be good to codify how this is done. There would need to be a simple way to toggle this render on/off without adding significant code to the final build. It could be as simple as having a file alongside main.ts(x) which can be imported with one line when desired. One key is to not conflict with any app rendering.

Summary

Gains

  • Easily see a component render in isolation from application
  • Can iterate a design on a static assertion template to get right DOM/css
  • Doesn't add much complexity to framework or build tooling

Drawbacks

  • Requires a lot of copy/paste
  • Could introduce poor testing coding which was written for deep render
  • May be hard to mock out other pieces of the application

@aciccarello
Copy link
Contributor Author

I tried the approach of wrapping the test harness and was able to get a POC working with intern. I'm not sure how I feel exactly about the API but it shows promise.

Use in test

Essentially wraps harness and overrides expectation. We need to set the timeout so we have time to view the page and the expectation is async.

const { describe, it } = intern.getInterface('bdd');

import { v, w } from '@dojo/framework/widget-core/d';
import harness from '@dojo/framework/testing/harness';
import assertionTemplate from '@dojo/framework/testing/assertionTemplate';
import HelloWorld from '../../../src/widgets/HelloWorld';
import renderHarness from './renderHarness';

describe('HelloWorld', () => {
	it('should render', async function() {
		const h = harness(() => w(HelloWorld, {}));
		(this as any).timeout = 1000 * 60 * 5;
		await renderHarness(h).expect(() => v('h1', { title: 'I am a title!' }, [ 'Biz-E-Bodies' ]));
	});

	it('should render a name', async function() {
		const template = assertionTemplate(() => v('h1', { title: 'I am a title!' }, [ 'Testing utils!' ]));
		const h = harness(() => w(HelloWorld, {name: 'Testing utils!'}));
		(this as any).timeout = 1000 * 60 * 5;
		await renderHarness(h).expect(template);
	});
});

Wrapper implementation

We check if we are in a browser and if so we render the expectation and actual render to the screen. We use a button to stall the test so we wire up a simple promise.

import { HarnessAPI } from '@dojo/framework/testing/harness';
import renderer from '@dojo/framework/widget-core/vdom';
import RenderContainer, { RenderContainerProperties } from './renderContainer';
import { w } from '@dojo/framework/widget-core/d';

const isInBrowser = typeof navigator !== 'undefined' && !(navigator.userAgent.includes('Node.js') || navigator.userAgent.includes('jsdom'));

const properties: RenderContainerProperties = {
	expectation: () => 'No expectation set',
	actual: () => 'No actual set'
};
const r = renderer(() => w(RenderContainer, properties));
if (isInBrowser) {
	r.mount();
}

export function renderHarness(h: HarnessAPI) {
	return {
		...h,
		async expect(render: Parameters<HarnessAPI['expect']>[0], options?: Parameters<HarnessAPI['expect']>[1]) {
			if (isInBrowser) {
				await new Promise((resolve) => {
					properties.continue = () => resolve();
					properties.expectation = render;
					properties.actual = h.getRender;
					r.invalidate();
				});
			}
			return h.expect(render, options);
		}
	};
}

export default renderHarness;

Wrapper

This is something to display both and a button to continue.

import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { v } from '@dojo/framework/widget-core/d';

export interface RenderContainerProperties {
	expectation: Function;
	actual: Function;
	continue?: Function;
}

export default class RenderContainer extends WidgetBase<RenderContainerProperties> {
	continue() {
		this.properties.continue && this.properties.continue();
	}
	protected render() {
		return v('div', [
			v('button', { onclick: () => { this.continue(); } }, [ 'Continue' ]),
			v('h2', ['Expected']),
			this.properties.expectation(),
			v('h2', ['Actual']),
			this.properties.actual()
		]);
	}
}

Demo

Demonstrates running locally (node & chrome) with tests passing and showing the results of each expectation.
dojo-test-render-poc

@agubler agubler mentioned this issue Sep 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: testing Testing enhancement New feature or request next Issue/Pull Request for the next major version
Projects
None yet
Development

No branches or pull requests

2 participants