Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
.. index::
2
    single: Cookbook; Mocking class within class
3
 
4
.. _mocking-class-within-class:
5
 
6
Mocking class within class
7
==========================
8
 
9
Imagine a case where you need to create an instance of a class and use it
10
within the same method:
11
 
12
.. code-block:: php
13
 
14
    // Point.php
15
    <?php
16
    namespace App;
17
 
18
    class Point {
19
        public function setPoint($x, $y) {
20
            echo "Point (" . $x . ", " . $y . ")" . PHP_EOL;
21
        }
22
    }
23
 
24
    // Rectangle.php
25
    <?php
26
    namespace App;
27
    use App\Point;
28
 
29
    class Rectangle {
30
        public function create($x1, $y1, $x2, $y2) {
31
            $a = new Point();
32
            $a->setPoint($x1, $y1);
33
 
34
            $b = new Point();
35
            $b->setPoint($x2, $y1);
36
 
37
            $c = new Point();
38
            $c->setPoint($x2, $y2);
39
 
40
            $d = new Point();
41
            $d->setPoint($x1, $y2);
42
 
43
            $this->draw([$a, $b, $c, $d]);
44
        }
45
 
46
        public function draw($points) {
47
            echo "Do something with the points";
48
        }
49
    }
50
 
51
And that you want to test that a logic in ``Rectangle->create()`` calls
52
properly each used thing - in this case calls ``Point->setPoint()``, but
53
``Rectangle->draw()`` does some graphical stuff that you want to avoid calling.
54
 
55
You set the mocks for ``App\Point`` and ``App\Rectangle``:
56
 
57
.. code-block:: php
58
 
59
    <?php
60
    class MyTest extends PHPUnit\Framework\TestCase {
61
        public function testCreate() {
62
            $point = Mockery::mock("App\Point");
63
            // check if our mock is called
64
            $point->shouldReceive("setPoint")->andThrow(Exception::class);
65
 
66
            $rect = Mockery::mock("App\Rectangle")->makePartial();
67
            $rect->shouldReceive("draw");
68
 
69
            $rect->create(0, 0, 100, 100);  // does not throw exception
70
            Mockery::close();
71
        }
72
    }
73
 
74
and the test does not work. Why? The mocking relies on the class not being
75
present yet, but the class is autoloaded therefore the mock alone for
76
``App\Point`` is useless which you can see with ``echo`` being executed.
77
 
78
Mocks however work for the first class in the order of loading i.e.
79
``App\Rectangle``, which loads the ``App\Point`` class. In more complex example
80
that would be a single point that initiates the whole loading (``use Class``)
81
such as::
82
 
83
    A        // main loading initiator
84
    |- B     // another loading initiator
85
    |  |-E
86
    |  +-G
87
    |
88
    |- C     // another loading initiator
89
    |  +-F
90
    |
91
    +- D
92
 
93
That basically means that the loading prevents mocking and for each such
94
a loading initiator there needs to be implemented a workaround.
95
Overloading is one approach, however it pollutes the global state. In this case
96
we try to completely avoid the global state pollution with custom
97
``new Class()`` behavior per loading initiator and that can be mocked easily
98
in few critical places.
99
 
100
That being said, although we can't stop loading, we can return mocks. Let's
101
look at ``Rectangle->create()`` method:
102
 
103
.. code-block:: php
104
 
105
    class Rectangle {
106
        public function newPoint() {
107
            return new Point();
108
        }
109
 
110
        public function create($x1, $y1, $x2, $y2) {
111
            $a = $this->newPoint();
112
            $a->setPoint($x1, $y1);
113
            ...
114
        }
115
        ...
116
    }
117
 
118
We create a custom function to encapsulate ``new`` keyword that would otherwise
119
just use the autoloaded class ``App\Point`` and in our test we mock that function
120
so that it returns our mock:
121
 
122
.. code-block:: php
123
 
124
    <?php
125
    class MyTest extends PHPUnit\Framework\TestCase {
126
        public function testCreate() {
127
            $point = Mockery::mock("App\Point");
128
            // check if our mock is called
129
            $point->shouldReceive("setPoint")->andThrow(Exception::class);
130
 
131
            $rect = Mockery::mock("App\Rectangle")->makePartial();
132
            $rect->shouldReceive("draw");
133
 
134
            // pass the App\Point mock into App\Rectangle as an alternative
135
            // to using new App\Point() in-place.
136
            $rect->shouldReceive("newPoint")->andReturn($point);
137
 
138
            $this->expectException(Exception::class);
139
            $rect->create(0, 0, 100, 100);
140
            Mockery::close();
141
        }
142
    }
143
 
144
If we run this test now, it should pass. For more complex cases we'd find
145
the next loader in the program flow and proceed with wrapping and passing
146
mock instances with predefined behavior into already existing classes.