Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing JavaScript

Testing JavaScript

Several years of experience and thought on testing JavaScript code. Some dos and don'ts, general guidelines on how to start and basic terminology.

#javascript #meetjsktw #meetjs #unittesting #testing #jest

License: CC Attribution-NonCommercial License

Jakub Synowiec

November 30, 2016
Tweet

More Decks by Jakub Synowiec

Other Decks in Technology

Transcript

  1. Testing The process consisting of all life cycle activities, both

    static and dynamic, concerned with planning, preparation and evaluation of software products and related work products to determine that they satisfy specified requirements, to demonstrate that they are fit for purpose and to detect defects. Jakub Synowiec @jakubsynowiec
  2. Testing An investigation conducted to provide stakeholders with information about

    the quality of the product or service. Jakub Synowiec @jakubsynowiec
  3. testing acceptance testing alpha testing beta testing safety testing accuracy

    testing suitability testing exploratory testing integration testing interface testing component testing negative testing load testing recovery testing failover testing stress testing data flow testing regression testing scalability testing user story testing big-bang testing top-down testing bottom-up testing Jakub Synowiec @jakubsynowiec active testing age testing all-pairs testing boundary value testing component testing compilance testing conversion testing domain testing endurance testing fuzz testing loop testing penetration testing sanity testing smoke testing static testing
  4. Unit Testing automated Automated test comparison of actual results from

    testing of individual software components (units) with expected results. Jakub Synowiec @jakubsynowiec
  5. Software unit Software units are software items that can't be

    split into sub-items (IEC 62304) Jakub Synowiec @jakubsynowiec
  6. Software unit • a set of procedures or functions, in

    a procedural or functional language, • a class and its nested classes, in an object or object-oriented language, • a set of procedures/functions/classes grouped to implement a pattern, Jakub Synowiec @jakubsynowiec
  7. Software unit • a set of procedures or functions, in

    a procedural or functional language, • a class and its nested classes, in an object or object-oriented language, • a set of procedures/functions/classes grouped to implement a pattern, Jakub Synowiec @jakubsynowiec
  8. Software unit /** * Gets the last element of `array`.

    * * @static * @memberOf _ * @since 0.1.0 * @category Array * @param {Array} array The array to query. * @returns {*} Returns the last element of `array`. * @example * * _.last([1, 2, 3]); * ! " 3 # function last(array) { var length = array ? array.length : 0; return length ? array[length - 1] : undefined; } module.exports = last; Jakub Synowiec @jakubsynowiec
  9. Software unit At a high-level, unit testing refers to the

    practice of testing certain functions and areas – or units – of our code. Jakub Synowiec @jakubsynowiec
  10. Test Suite A set of several test cases for a

    component or system under test, where the post condition of one test is often used as the precondition for the next one. Test Condition An item or event of a component or system that could be verified by one or more test cases, e.g. a function, transaction, feature, quality attribute, or structural element. Test Case A set of input values, execution preconditions, expected results and execution postconditions, developed for a particular objective or test condition, such as to exercise a particular program path or to verify compliance with a specific requirement. Test Comparison The process of identifying differences between the actual results produced by the component or system under test and the expected results for a test. Jakub Synowiec @jakubsynowiec
  11. Jakub Synowiec @jakubsynowiec describe('Observable', () " { ! test suite

    it('Should be subscribable', () " { ! test case const instance = new ko.observable(); expect(ko.isSubscribable(instance)).toEqual(true); ! test comparison }); it('Should advertise that instances are observable', () " { ! test case const instance = new ko.observable(); expect(ko.isObservable(instance)).toEqual(true); ! test comparison }); ! $ }); Test suite
  12. Jakub Synowiec @jakubsynowiec describe('Observable', () " { ! test suite

    it('Should be subscribable', () " { ! test case const instance = new ko.observable(); expect(ko.isSubscribable(instance)).toEqual(true); ! test comparison }); it('Should advertise that instances are observable', () " { ! test case const instance = new ko.observable(); expect(ko.isObservable(instance)).toEqual(true); ! test comparison }); ! $ }); Test case
  13. Jakub Synowiec @jakubsynowiec describe('Observable', () " { ! test suite

    it('Should be subscribable', () " { ! test case const instance = new ko.observable(); expect(ko.isSubscribable(instance)).toEqual(true); ! test comparison }); it('Should advertise that instances are observable', () " { ! test case const instance = new ko.observable(); expect(ko.isObservable(instance)).toEqual(true); ! test comparison }); ! $ }); Test comparison
  14. Jakub Synowiec @jakubsynowiec describe('Observable', () " { ! test suite

    it('Should be subscribable', () " { ! test case const instance = new ko.observable(); expect(ko.isSubscribable(instance)).toEqual(true); ! test comparison }); it('Should advertise that instances are observable', () " { ! test case const instance = new ko.observable(); expect(ko.isObservable(instance)).toEqual(true); ! test comparison }); ! $ }); Unit test
  15. Unit testing makes developer lives easier* • easier to find

    bugs, • easier to maintain, • easier to understand, • easier to develop. Jakub Synowiec @jakubsynowiec
  16. Easier to understand Reading tests can be a great way

    of understanding how things work and should be used. It’s often better than the documentation. Jakub Synowiec @jakubsynowiec
  17. What is needed • proper approach to tests design, •

    tests following the AAA system, • tests without branching or looping, • doing test reviews, Jakub Synowiec @jakubsynowiec
  18. Test-After Jakub Synowiec @jakubsynowiec • If you write your API

    and then you write your tests against it, you often realise that your API isn't what it should be.
  19. TDD Jakub Synowiec @jakubsynowiec • TDD is OK if you

    don’t know the algorithm, you can use TDD for algorithm triangulation,
  20. Test-First Jakub Synowiec @jakubsynowiec • You are thinking about your

    code from the standpoint of the consumer of your code, • Often you discover something that you didn't know that was important, that'll make the code better, • You find that if you designed something that's hard to test, it's usually because you haven't designed it very well. It has strayed from its purpose, trying to do too much.
  21. Unit test should be Jakub Synowiec @jakubsynowiec • Trustworthy &

    consistent, • Repeatable, • Easy to write & read, • Fast & short, • Separated,
  22. Jakub Synowiec @jakubsynowiec Test name is the first thing you

    read QUnit.test( "jQuery('massive html #7990')", function( assert ) { assert.expect( 3 ); var i, li = "<li>very very very very large html string %li>", html = [ "<ul>" ]; for ( i = 0; i < 30000; i += 1 ) { html[ html.length ] = li; } html[ html.length ] = " %ul>"; html = jQuery( html.join( "" ) )[ 0 ]; assert.equal( html.nodeName.toLowerCase(), "ul" ); assert.equal( html.firstChild.nodeName.toLowerCase(), "li" ); assert.equal( html.childNodes.length, 30000 ); } );
  23. Jakub Synowiec @jakubsynowiec Loops and branches == bugs QUnit.test( "jQuery('massive

    html #7990')", function( assert ) { assert.expect( 3 ); var i, li = "<li>very very very very large html string %li>", html = [ "<ul>" ]; for ( i = 0; i < 30000; i += 1 ) { html[ html.length ] = li; } html[ html.length ] = " %ul>"; html = jQuery( html.join( "" ) )[ 0 ]; assert.equal( html.nodeName.toLowerCase(), "ul" ); assert.equal( html.firstChild.nodeName.toLowerCase(), "li" ); assert.equal( html.childNodes.length, 30000 ); } );
  24. Jakub Synowiec @jakubsynowiec No commented out tests ! can actually

    yield more than one, when iframes are included, the window is an array as well assert.equal( jQuery( window ).length, 1, "Correct number of elements generated for jQuery(window)" ); & ! disabled since this test was doing nothing. i tried to fix it but i'm not sure ! what the expected behavior should even be. FF returns "\n" for the text node ! make sure this is handled var crlfContainer = jQuery('<p>\r\n %p>'); var x = crlfContainer.contents().get(0).nodeValue; assert.equal( x, what???, "Check for 'r and 'n in jQuery()" ); # & ! Disabled until we add this functionality in var pass = true; try { jQuery("<div>Testing % div>").appendTo(document.getElementById("iframe").contentDocument.body); } catch(e){ pass = false; } assert.ok( pass, "jQuery('&lt;tag&gt;') needs optional document parameter to ease cross-frame DOM wrangling, see #968" ); # assert.equal( code.length, 1, "Correct number of elements generated for code" );
  25. Jakub Synowiec @jakubsynowiec Do not use test case as test

    suite QUnit.test( "attr(String)", function( assert ) { assert.expect( 50 ); var extras, body, $body, select, optgroup, option, $img, styleElem, $button, $form, $a; assert.equal( jQuery( "#text1" ).attr( "type" ), "text", "Check for type attribute" ); assert.equal( jQuery( "#radio1" ).attr( "type" ), "radio", "Check for type attribute" ); assert.equal( jQuery( "#check1" ).attr( "type" ), "checkbox", "Check for type attribute" ); assert.equal( jQuery( "#simon1" ).attr( "rel" ), "bookmark", "Check for rel attribute" ); assert.equal( jQuery( "#google" ).attr( "title" ), "Google!", "Check for title attribute" ); assert.equal( jQuery( "#mark" ).attr( "hreflang" ), "en", "Check for hreflang attribute" ); assert.equal( jQuery( "#en" ).attr( "lang" ), "en", "Check for lang attribute" ); assert.equal( jQuery( "#simon" ).attr( "class" ), "blog link", "Check for class attribute" ); assert.equal( jQuery( "#name" ).attr( "name" ), "name", "Check for name attribute" ); assert.equal( jQuery( "#text1" ).attr( "name" ), "action", "Check for name attribute" ); assert.ok( jQuery( "#form" ).attr( "action" ).indexOf( "formaction" ) ) 0, "Check for action attribute" ); assert.equal( jQuery( "#text1" ).attr( "value", "t" ).attr( "value" ), "t", "Check setting the value attribute" ); assert.equal( jQuery( "#text1" ).attr( "value", "" ).attr( "value" ), "", "Check setting the value attribute to empty string" ); assert.equal( jQuery( "<div value='t'> %div>" ).attr( "value" ), "t", "Check setting custom attr named 'value' on a div" ); assert.equal( jQuery( "#form" ).attr( "blah", "blah" ).attr( "blah" ), "blah", "Set non-existent attribute on a form" ); assert.equal( jQuery( "#foo" ).attr( "height" ), undefined, "Non existent height attribute should return undefined" ); ! [7472] & [3113] (form contains an input with name="action" or name="id") extras = jQuery( "<input id='id' name='id' *<input id='name' name='name' *<input id='target' name='target' *" ).appendTo( "#testForm" ); assert.equal( jQuery( "#form" ).attr( "action", "newformaction" ).attr( "action" ), "newformaction", "Check that action attribute was changed" ); assert.equal( jQuery( "#testForm" ).attr( "target" ), undefined, "Retrieving target does not equal the input with name=target" ); assert.equal( jQuery( "#testForm" ).attr( "target", "newTarget" ).attr( "target" ), "newTarget", "Set target successfully on a form" ); assert.equal( jQuery( "#testForm" ).removeAttr( "id" ).attr( "id" ), undefined, "Retrieving id does not equal the input with name=id after id is removed [#7472]" ); ! Bug #3685 (form contains input with name="name") assert.equal( jQuery( "#testForm" ).attr( "name" ), undefined, "Retrieving name does not retrieve input with name=name" ); extras.remove(); assert.equal( jQuery( "#text1" ).attr( "maxlength" ), "30", "Check for maxlength attribute" ); assert.equal( jQuery( "#text1" ).attr( "maxLength" ), "30", "Check for maxLength attribute" ); assert.equal( jQuery( "#area1" ).attr( "maxLength" ), "30", "Check for maxLength attribute" ); ! using innerHTML in IE causes href attribute to be serialized to the full path jQuery( "<a *" ).attr( { "id": "tAnchor5", "href": "#5" } ).appendTo( "#qunit-fixture" ); assert.equal( jQuery( "#tAnchor5" ).attr( "href" ), "#5", "Check for non-absolute href (an anchor)" ); jQuery( "<a id='tAnchor6' href='#5' *" ).appendTo( "#qunit-fixture" ); assert.equal( jQuery( "#tAnchor5" ).prop( "href" ), jQuery( "#tAnchor6" ).prop( "href" ), "Check for absolute href prop on an anchor" ); jQuery( "<script type='jquery/test' src='#5' id='scriptSrc'> %script>" ).appendTo( "#qunit-fixture" ); assert.equal( jQuery( "#tAnchor5" ).prop( "href" ), jQuery( "#scriptSrc" ).prop( "src" ), "Check for absolute src prop on a script" ); ! list attribute is readonly by default in browsers that support it jQuery( "#list-test" ).attr( "list", "datalist" ); assert.equal( jQuery( "#list-test" ).attr( "list" ), "datalist", "Check setting list attribute" ); ! Related to [5574] and [5683] body = document.body; $body = jQuery( body ); assert.strictEqual( $body.attr( "foo" ), undefined, "Make sure that a non existent attribute returns undefined" ); body.setAttribute( "foo", "baz" ); assert.equal( $body.attr( "foo" ), "baz", "Make sure the dom attribute is retrieved when no expando is found" ); $body.attr( "foo", "cool" ); assert.equal( $body.attr( "foo" ), "cool", "Make sure that setting works well when both expando and dom attribute are available" ); body.removeAttribute( "foo" ); ! Cleanup select = document.createElement( "select" ); optgroup = document.createElement( "optgroup" ); option = document.createElement( "option" ); optgroup.appendChild( option ); select.appendChild( optgroup ); assert.equal( jQuery( option ).prop( "selected" ), true, "Make sure that a single option is selected, even when in an optgroup." ); $img = jQuery( "<img style='display:none' width='215' height='53' src='data/1x1.jpg' *" ).appendTo( "body" ); assert.equal( $img.attr( "width" ), "215", "Retrieve width attribute on an element with display:none." ); assert.equal( $img.attr( "height" ), "53", "Retrieve height attribute on an element with display:none." ); ! Check for style support styleElem = jQuery( "<div *" ).appendTo( "#qunit-fixture" ).css( { background: "url(UPPERlower.gif)" } ); assert.ok( ,~styleElem.attr( "style" ).indexOf( "UPPERlower.gif" ), "Check style attribute getter" ); assert.ok( ,~styleElem.attr( "style", "position:absolute;" ).attr( "style" ).indexOf( "absolute" ), "Check style setter" ); ! Check value on button element (#1954) $button = jQuery( "<button>text %button>" ).insertAfter( "#button" ); assert.strictEqual( $button.attr( "value" ), undefined, "Absence of value attribute on a button" ); assert.equal( $button.attr( "value", "foobar" ).attr( "value" ), "foobar", "Value attribute on a button does not return innerHTML" ); assert.equal( $button.attr( "value", "baz" ).html(), "text", "Setting the value attribute does not change innerHTML" ); ! Attributes with a colon on a table element (#1591) assert.equal( jQuery( "#table" ).attr( "test:attrib" ), undefined, "Retrieving a non-existent attribute on a table with a colon does not throw an error." ); assert.equal( jQuery( "#table" ).attr( "test:attrib", "foobar" ).attr( "test:attrib" ), "foobar", "Setting an attribute on a table with a colon does not throw an error." ); $form = jQuery( "<form class='something'> %form>" ).appendTo( "#qunit-fixture" ); assert.equal( $form.attr( "class" ), "something", "Retrieve the class attribute on a form." ); $a = jQuery( "<a href='#' onclick='something()'>Click %a>" ).appendTo( "#qunit-fixture" ); assert.equal( $a.attr( "onclick" ), "something()", "Retrieve ^on attribute without anonymous function wrapper." ); assert.ok( jQuery( "<div *" ).attr( "doesntexist" ) - undefined, "Make sure undefined is returned when no attribute is found." ); assert.ok( jQuery( "<div *" ).attr( "title" ) - undefined, "Make sure undefined is returned when no attribute is found." ); assert.equal( jQuery( "<div *" ).attr( "title", "something" ).attr( "title" ), "something", "Set the title attribute." ); assert.ok( jQuery().attr( "doesntexist" ) - undefined, "Make sure undefined is returned when no element is there." ); assert.equal( jQuery( "<div *" ).attr( "value" ), undefined, "An unset value on a div returns undefined." ); assert.strictEqual( jQuery( "<select><option value='property'> %option> %select>" ).attr( "value" ), undefined, "An unset value on a select returns undefined." ); $form = jQuery( "#form" ).attr( "enctype", "multipart/form-data" ); assert.equal( $form.prop( "enctype" ), "multipart/form-data", "Set the enctype of a form (encoding in IE6/7 #6743)" ); } ); 116 lines, 50 test cases
  26. Arrange, Act, Assert Jakub Synowiec @jakubsynowiec describe('arrayFirst', () " {

    var matchB; beforeEach(() " { ! arrange matchB = jasmine.createSpy('matchB').andCallFake(x " x.charAt(0) - 'b'); }); it('Should return the first matching element from the input array', () " { var result = ko.utils.arrayFirst(['a', 'b', 'c', 'b2'], matchB); ! act expect(result).toBe('b'); ! assert }); ! $ });
  27. Arrange Jakub Synowiec @jakubsynowiec describe('arrayFirst', () " { var matchB;

    beforeEach(() " { ! arrange matchB = jasmine.createSpy('matchB').andCallFake(x " x.charAt(0) - 'b'); }); it('Should return the first matching element from the input array', () " { var result = ko.utils.arrayFirst(['a', 'b', 'c', 'b2'], matchB); ! act expect(result).toBe('b'); ! assert }); ! $ });
  28. Act Jakub Synowiec @jakubsynowiec describe('arrayFirst', () " { var matchB;

    beforeEach(() " { ! arrange matchB = jasmine.createSpy('matchB').andCallFake(x " x.charAt(0) - 'b'); }); it('Should return the first matching element from the input array', () " { var result = ko.utils.arrayFirst(['a', 'b', 'c', 'b2'], matchB); ! act expect(result).toBe('b'); ! assert }); ! $ });
  29. Assert Jakub Synowiec @jakubsynowiec describe('arrayFirst', () " { var matchB;

    beforeEach(() " { ! arrange matchB = jasmine.createSpy('matchB').andCallFake(x " x.charAt(0) - 'b'); }); it('Should return the first matching element from the input array', () " { var result = ko.utils.arrayFirst(['a', 'b', 'c', 'b2'], matchB); ! act expect(result).toBe('b'); ! assert }); ! $ });
  30. Short Jakub Synowiec @jakubsynowiec describe('arrayFirst', () " { var matchB;

    beforeEach(() " { ! arrange matchB = jasmine.createSpy('matchB').andCallFake(x " x.charAt(0) - 'b'); }); it('Should return the first matching element from the input array', () " { var result = ko.utils.arrayFirst(['a', 'b', 'c', 'b2'], matchB); ! act expect(result).toBe('b'); ! assert }); ! $ });
  31. Test Separation Jakub Synowiec @jakubsynowiec • Isolation - sociable vs

    solitary tests, • Full control of dependencies, sociable test solitary test
  32. Jakub Synowiec @jakubsynowiec Test Doubles • Test Double is simply

    another object that conforms to the interface of the required Collaborator, and can be passed in in its place, • Dummy is an object that simply implements an Interface, and does nothing else, • Stub is a Dummy that returns responses in order to allow to make assertions, • Spies are Stubs that maintain state and allow to check the parameters or total number of times a method was called, • Fakes not only return values, but they also work like a real Collaborator. Usually a Fake has some sort of a shortcut unusable in production.
  33. Jakub Synowiec @jakubsynowiec var matchB; beforeEach(() " { matchB =

    jasmine .createSpy('matchB') ! spy .andCallFake(x " x.charAt(0) - 'b'); ! stub }); Test Double
  34. Jakub Synowiec @jakubsynowiec var matchB; beforeEach(() " { matchB =

    jasmine .createSpy('matchB') ! spy .andCallFake(x " x.charAt(0) - 'b'); ! stub }); Stub
  35. Jakub Synowiec @jakubsynowiec var matchB; beforeEach(() " { matchB =

    jasmine .createSpy('matchB') ! spy .andCallFake(x " x.charAt(0) - 'b'); ! stub }); Spy
  36. Jakub Synowiec @jakubsynowiec Mocks are not Stubs Mocks are test

    doubles that make assertions on behaviour. Mocks use behaviour verification, where we instead (of asserting results) check to see if the correct calls on the mocked Collaborator were made. We do this check by telling the mock what to expect during setup and asking the mock to verify itself during verification.
  37. Jakub Synowiec @jakubsynowiec Setup Mock it('should pass object with correct

    values to save only once', () " { var user = { name: 'test' }; var expectedUser = { name: user.name, nameLowercase: user.name.toLowerCase() }; ! mock setup var database = sinon.mock(Database); database.expects('save').once().withArgs(expectedUser); ! eof mock setup setupNewUser(user); ! verify mock database.verify(); database.restore(); });
  38. Jakub Synowiec @jakubsynowiec Verify Mock it('should pass object with correct

    values to save only once', () " { var user = { name: 'test' }; var expectedUser = { name: user.name, nameLowercase: user.name.toLowerCase() }; ! mock setup var database = sinon.mock(Database); database.expects('save').once().withArgs(expectedUser); ! eof mock setup setupNewUser(user); ! verify mock database.verify(); database.restore(); });
  39. Jakub Synowiec @jakubsynowiec Test Doubles - when to use •

    You should use Stubs (or Stubs + Spies) to isolate units from dependencies (internal APIs), • You can use Fakes to simplify Collaborators, e.g. in memory database, • You should only use Mocks to mock external APIs, e.g. remote HTTP calls,
  40. Test Review Jakub Synowiec @jakubsynowiec • Understand intent of the

    developer, • Verify understanding of the business value, • Many times quicker than a full code review, • You can drill-down if needed,
  41. 4 main excuses • It costs too much time, we’d

    better fix these bugs first, • Our system/code is too big for unit tests, • We just ought to be more careful not to introduce bugs, that will help us a lot, • Our system is not suited for unit tests. Jakub Synowiec @jakubsynowiec
  42. Too much time Yes, it costs time to design and

    write tests, but you will win that time back, along with other benefits. Jakub Synowiec @jakubsynowiec
  43. System is to big If you start writing tests now,

    steadily your codebase will be covered more and more by tests. Jakub Synowiec @jakubsynowiec
  44. Should be more careful Everybody writes bugs, we all write

    bugs. The more complex problem, the more mistakes are made. But if you can detect it, you can fix it before it does any damage. Jakub Synowiec @jakubsynowiec
  45. System is not suited This one might actually be true,

    but “Your special case isn’t special enough”. If your units are not separated and are tightly coupled or your units are not „pure”, then your system is poorly designed and hardly testable. But if its poorly designed, it is likely to break and modifying it will be super painful. Jakub Synowiec @jakubsynowiec
  46. Save the world… A good test reads like a little

    story: I set something up, I change it, I test it. Jakub Synowiec @jakubsynowiec
  47. …one unit at a time. When implementing a new functionality

    (or refactoring an old one), first think about the desired results, necessary parameters and required interfaces. Write your tests while designing your code (Test-First). Your tests should guide you through the implementation. Jakub Synowiec @jakubsynowiec
  48. Write tests Jakub Synowiec @jakubsynowiec 1. Select unit, design your

    tests, 2. Arrange for testing: 1. declare local variables, 2. set state, 3. load fixtures, 4. stub internal APIs, 5. mock external APIs, 3. Act - execute tested unit, 4. Assert - compare actual results with expected results, 5. Repeat,
  49. When it’s hard to test. If something is hard to

    test, it's usually because you haven't designed it very well. It has strayed from its purpose, trying to do too much. Split it to smaller units, extract some functionality to modules. Jakub Synowiec @jakubsynowiec
  50. When it’s hard to test… return new Promise((resolve, reject) "

    { request ! $ .end((err, res) " { if (res . res.ok) { resolve(res.body.reduce((acc, current, userId) " { userId = decode(jwt).user.id; if (current.device . current.device.imei) { if (current.owner.id - userId / (current.state . Object.keys(current.state).length > 0)) { acc.push(current.device.imei); } } return acc; }, [])); } else { handleError(err, reject); } }); }); Jakub Synowiec @jakubsynowiec
  51. …extract unit return new Promise((resolve, reject) " { request !

    $ .end((err, res) " { if (res . res.ok) { resolve(getUserAccessibleDevices(res.body, decode(jwt).user.id)); } else { handleError(err, reject); } }); }); Jakub Synowiec @jakubsynowiec
  52. Coverage, what coverage? Strive for 100%, because then you’ll hit

    80-ish %, which is a good number, but if you say 80%, you're gonna end with 40%. But it depends…. Jakub Synowiec @jakubsynowiec
  53. When enough is enough Knowing when not to test is

    kind of the mark of experience and wisdom when it comes to testing. You should do it too much, and then learn to back off. Jakub Synowiec @jakubsynowiec