Hello, my name is Dmitry Karlovsky and, unfortunately, I have no time to write a great article, but I really want to share some ideas. So let me test a little note about programming on you. Today we will talk about automated testing:
From more important to less:
In any case, I emphasize that we are talking exclusively about automated testing.
The more tests, the slower the development.
The more complete the tests, the faster the refactoring and testing, and as a result, the delivery of new functionality.
Depending on priorities , there are several basic strategies:
So that my analytics is not completely unfounded, let's create the simplest application of two components. It will contain a name entry field and a block with a welcome message addressed to this name.
$my_hello $mol_list rows / <= Input $mol_string value?val <=> name?val \ <= Output $my_hello_message target <= name - $my_hello_message $mol_view sub / \Hello, <= target \
Those who are not familiar with this notation, I suggest to look at the equivalent TypeScript code:
export class $my_hello extends $mol_list { rows() { return [ this.Input() , this.Output() ] } @mem Input() { return this.$.$mol_string.make({ value : next => this.name( next ) , }) } @mem Output() { return this.$.$my_hello_message.make({ target : ()=> this.name() , }) } @mem name( next = '' ) { return next } } export class $my_hello_message extends $mol_view { sub() { return [ 'Hello, ' , this.target() ] } target() { return '' } }
@mem
is a reactive caching decorator. this.$
- di-context. Binding occurs through property overrides. .make
simply creates an instance and overrides the specified properties.
With this approach, we use real dependencies whenever possible.
What should mock up anyway:
So, first we write a test for the embedded component:
// Components tests of $my_hello_message $mol_test({ 'print greeting to defined target'() { const app = new $my_hello_message app.target = ()=> 'Jin' $mol_assert_equal( app.sub().join( '' ) , 'Hello, Jin' ) } , })
And now we add tests to the external component:
// Components tests of $my_hello $mol_test({ 'contains Input and Output'() { const app = new $my_hello $mol_assert_like( app.sub() , [ app.Input() , app.Output() , ] ) } , 'print greeting with name from input'() { const app = new $my_hello $mol_assert_equal( app.Output().sub().join( '' ) , 'Hello, ' ) app.Input().value( 'Jin' ) $mol_assert_equal( app.Output().sub().join( '' ), 'Hello, Jin' ) } , })
As you can see, all we need is a public interface component. Pay attention, we don't care what property is and how the value is transferred to Output. We check exactly the requirements: so that the displayed greeting matches the name entered by the user.
For unit tests, it is necessary to isolate the module from the rest of the code. When a module does not interact with other modules, the tests are the same as the component ones:
// Unit tests of $my_hello_message $mol_test({ 'print greeting to defined target'() { const app = new $my_hello_message app.target = ()=> 'Jin' $mol_assert_equal( app.sub().join( '' ), 'Hello, Jin' ) } , })
If the module needs other modules, they are replaced by plugs and we check that communication with them is as expected.
// Unit tests of $my_hello $mol_test({ 'contains Input and Output'() { const app = new $my_hello const Input = {} as $mol_string app.Input = ()=> Input const Output = {} as $mol_hello_message app.Output = ()=> Output $mol_assert_like( app.sub() , [ Input , Output , ] ) } , 'Input value binds to name'() { const app = new $my_hello app.$ = Object.create( $ ) const Input = {} as $mol_string app.$.$mol_string = function(){ return Input } as any $mol_assert_equal( app.name() , '' ) Input.value( 'Jin' ) $mol_assert_equal( app.name() , 'Jin' ) } , 'Output target binds to name'() { const app = new $my_hello app.$ = Object.create( $ ) const Output = {} as $my_hello_message app.$.$mol_hello_message = function(){ return Output } as any $mol_assert_equal( Output.title() , '' ) app.name( 'Jin' ) $mol_assert_equal( Output.title() , 'Jin' ) } , })
Mocking is not free - it leads to more complicated tests. But the saddest thing is that having checked the work with mocks, you cannot be sure that with real modules it will all work correctly. If you were attentive, you already noticed that in the last code we expect that the name should be passed through the title
property. And this leads us to two types of errors:
And finally, the tests, it turns out, do not check the requirements (let me remind you that the greeting with the substituted name should be displayed), and the implementation (such a method is called inside with such and such parameters). This means that tests are fragile.
Fragile tests are tests that break down at equivalent implementation changes.
Equivalent changes are such implementation changes that do not break the code’s compliance with functional requirements.
The TDD algorithm is quite simple and quite useful:
If we write fragile tests, then at the refactor step they will constantly fall, requiring research and adjustment, which reduces the programmer's productivity.
To overcome the cases remaining after the modular tests, they invented an additional type of tests - integration tests. Here we take several modules and check that they interact correctly:
// Integration tests of $my_hello $mol_test({ 'print greeting with name'() { const app = new $my_hello $mol_assert_equal( app.Output().sub().join( '' ) , 'Hello, ' ) app.Input().value( 'Jin' ) $mol_assert_equal( app.Output().sub().join( '' ), 'Hello, Jin' ) } , })
Yeah, we got that latest component test. In other words, we somehow wrote all the component tests that checked the requirements, but additionally recorded in the tests a specific implementation of logic. This is usually redundant.
Criteria | Cascaded component | Modular + Integrational |
---|---|---|
CLOS | 17 | 34 + 8 |
Complexity | Simple | Complex |
Incapsulation | Black box | White box |
Fragility | Low | High |
Coverage | Full | Extra |
Velocity | High | Low |
Duration | Low | High |
Source: https://habr.com/ru/post/351430/
All Articles