• No se han encontrado resultados

6. GENERACIÓN DE INDICADORES

6.3. PROCESO DE INYECCIÓN

Because toLetterGrade is pure, you can run it several times against different inputs to test many of its boundary conditions. Because it’s referentially transparent, you can also shift the order of these test cases without altering the result of the test. Later, you’ll learn an automated way of generating proper sample input; but for now, you’ll do this manually to see that the function works correctly against a comprehensive set of input. Now that all the individual pieces of the program have been tested, you can safely assume the program as a whole works, because it’s driven by the power of com-position and functional combinators.

Along the same lines, what about fork? Functional combinators don’t require much testing, because they contain no business logic other than orchestrating func-tion calls in your applicafunc-tion’s control flow. Recall from secfunc-tion 4.6 that combinators are useful for substituting standard control artifacts like if-else (alternation) and loops (sequence).

Some libraries implement combinators out of the box, like R.tap; but when using custom ones (like fork), you can test them independent of the rest of the application and apart from the business logic. For the sake of completeness, let’s write a quick test for fork that showcases another good use of R.identity:

QUnit.test('Functional Combinator: fork', function (assert) { const timesTwo = fork((x) => x + x, R.identity, R.identity);

assert.equal(timesTwo(1), 2);

assert.equal(timesTwo(2), 4);

});

Again, testing with a simple function is sufficient, because combinators are completely agnostic when it comes to the arguments provided. Using functional libraries, compo-sition, and combinators makes development and testing trivial; but things can get messy when you’re dealing with impure behavior.

6.3.3 Separating the pure from the impure with monadic isolation

In previous chapters, you learned that most programs have pure and impure parts.

This is especially true in client-side JavaScript, because interacting with the DOM is

what the language was meant for. On the server, you’ll have other requirements such as reading from a database or file. You learned how to use composition to combine the pure and impure functions that make up your programs. But this still made them impure; you relied on the IO monad to push the line of purity even further away so that you could obtain referential transparency from the application’s perspective, making it more declarative and easier to reason about. In addition to IO, you used other monads like Maybe and Either to create a surefire way to run programs that are still responsive in the event of failure. With all these techniques, you can control most side effects. But when your JavaScript code needs to read and write to the DOM, how can you guarantee that your tests remain isolated and repeatable?

Recall that the nonfunctional version of showStudent makes no effort to separate its impure parts: it’s all mixed together, so it will run as a whole on each and every test.

This is utterly inefficient and unproductive because you would need to run the entire program every time even when you only wanted to validate, say, that db.get(ssn) worked with different combinations of Social Security numbers. Another disadvantage is that you can’t test it thoroughly because all statements are tightly coupled. For instance, the first block of code will exit the function early with an exception and pre-vent you from testing db.get(ssn) against invalid input.

On the other hand, functional programming is aimed at reducing the involvement of operations that cause side effects (like IO) to minimal functions (simple reads and writes) so that you can increase the testable scope of your application logic while decoupling the boundaries of IO testing you aren’t responsible for. Let’s revisit the functional version of showStudent:

const showStudent = R.compose(

map(append('#student-info')), liftIO,

map(csv),

map(R.props(['ssn', 'firstname', 'lastname'])), chain(findStudent),

chain(checkLengthSsn), lift(cleanInput));

Looking closely at both programs, you can see how the functional version is essentially taking the imperative version apart and bolting it together with composition and monads. As a result, you dramatically increase the testable scope of showStudent and clearly recognize and isolate the pure functions from the impure (see figure 6.6).

Let’s analyze the testability of the components of showStudent. Of the five func-tions, only three can be tested reliably: cleanInput, checkLengthSsn, and csv. Although findStudent has side effects when reading data from external resources, you’ll see ways to get around this in a later section. The remaining function, append, has no real business logic because it’s been reduced to appending to the DOM whatever data is given to it. It’s not in your best interest, and it isn’t the best use of your time, to test DOMAPIs; leave that to browser manufacturers. With functional programming, you can take a hard-to-test program and split it into highly testable pieces.

Now, let’s compare this against the nonfunctional, tightly coupled code in listing 6.2. In the functional version, you’re able to test roughly 90% of the program reliably, whereas the imperative version has the same fate as the procedural increment func-tion—it fails on subsequent or out-of-order runs.

The following listing shows the unit tests for each testable component in figure 6.6.

QUnit.test('showStudent: cleanInput', function (assert) {

const input = ['', '-44-44-', '44444', ' 4 ', ' 4-4 '];

const assertions = ['', '4444', '44444', '4', '44'];

assert.expect(input.length);

input.forEach(function (val, key) {

assert.equal(cleanInput(val), assertions[key]);

});

});

QUnit.test('showStudent: checkLengthSsn', function (assert) { assert.ok(checkLengthSsn('444444444').isRight);

assert.ok(checkLengthSsn('').isLeft);

assert.ok(checkLengthSsn('44444444').isLeft);

assert.equal(checkLengthSsn('444444444').chain(R.length), 9);

});

QUnit.test('showStudent: csv', function (assert) { assert.equal(csv(['']), '');

assert.equal(csv(['Alonzo']), 'Alonzo');

assert.equal(csv(['Alonzo', 'Church']), 'Alonzo,Church');

assert.equal(csv(['Alonzo', '', 'Church']), 'Alonzo,,Church,');

});

Listing 6.2 Unit testing pure components of showStudent showStudent

Impure: can be tested by other means

Pure Pure

Figure 6.6 Identifying the testable areas of the showStudent program. The components that perform IO are impure and can’t be tested reliably because they contain side effects. Other than having impure parts, the scope of the entire program remains highly testable.

Using inputs of varying lengths and containing whitespace

Using Either.isLeft or Either.isRight to make assertions about the contents of the monad

Because these functions are isolated and thoroughly tested on their own (again, later I’ll show you an automated mechanism for generating input data), you can safely refactor them without fear of breaking things in other places.

You have one last function to test: findStudent. This function originates from the impure safeFindObject, which queries an external object storage to look up student records. But the side effects in this function are manageable by using a technique called mock objects.