Skip to content

Commit f5e84f8

Browse files
committed
Add unit tests for basic classes
These unit tests have been written for classes that required little to no changes in the source files to be testable.
1 parent d9bffbc commit f5e84f8

File tree

13 files changed

+1275
-4
lines changed

13 files changed

+1275
-4
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
*.iml
88
*.local.*
99
*.swp
10+
11+
# Tests
12+
.phpunit.result.cache

src/Report/ReportLoader.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88

99
class ReportLoader
1010
{
11+
private string $reportPath;
12+
13+
public function __construct(string $reportPath = Report::REPORT_FILENAME)
14+
{
15+
$this->reportPath = $reportPath;
16+
}
17+
1118
public function loadReport(): ?Report
1219
{
13-
if (file_exists(Report::REPORT_FILENAME)) {
14-
$contents = file_get_contents(Report::REPORT_FILENAME);
15-
$data = json_decode($contents, true, JSON_THROW_ON_ERROR);
20+
if (file_exists($this->reportPath)) {
21+
$contents = file_get_contents($this->reportPath);
22+
$data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);
1623
Assert::isArray($data);
1724
return Report::fromArray($data);
1825
}

src/Report/ReportWriter.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66

77
class ReportWriter
88
{
9+
private string $reportPath;
10+
11+
public function __construct(string $reportPath = Report::REPORT_FILENAME)
12+
{
13+
$this->reportPath = $reportPath;
14+
}
15+
916
public function write(Report $report): void
1017
{
11-
file_put_contents(Report::REPORT_FILENAME, json_encode($report->toArray()));
18+
file_put_contents($this->reportPath, json_encode($report->toArray()));
1219
}
1320
}
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypernode\Deploy\Tests\Unit\Console\Output;
6+
7+
use DateTime;
8+
use Hypernode\Deploy\Console\Output\ConsoleLogger;
9+
use PHPUnit\Framework\MockObject\MockObject;
10+
use PHPUnit\Framework\TestCase;
11+
use Psr\Log\InvalidArgumentException;
12+
use Psr\Log\LogLevel;
13+
use Symfony\Component\Console\Output\ConsoleOutputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
class ConsoleLoggerTest extends TestCase
17+
{
18+
private OutputInterface&MockObject $output;
19+
private ConsoleLogger $logger;
20+
21+
protected function setUp(): void
22+
{
23+
$this->output = $this->createMock(OutputInterface::class);
24+
$this->output->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_NORMAL);
25+
$this->logger = new ConsoleLogger($this->output);
26+
}
27+
28+
public function testLogInterpolatesStringPlaceholder(): void
29+
{
30+
$this->output->expects($this->once())
31+
->method('writeln')
32+
->with(
33+
$this->stringContains('Hello World'),
34+
$this->anything()
35+
);
36+
37+
$this->logger->info('Hello {name}', ['name' => 'World']);
38+
}
39+
40+
public function testLogInterpolatesIntegerPlaceholder(): void
41+
{
42+
$this->output->expects($this->once())
43+
->method('writeln')
44+
->with(
45+
$this->stringContains('Count: 42'),
46+
$this->anything()
47+
);
48+
49+
$this->logger->info('Count: {count}', ['count' => 42]);
50+
}
51+
52+
public function testLogInterpolatesFloatPlaceholder(): void
53+
{
54+
$this->output->expects($this->once())
55+
->method('writeln')
56+
->with(
57+
$this->stringContains('Value: 3.14'),
58+
$this->anything()
59+
);
60+
61+
$this->logger->info('Value: {value}', ['value' => 3.14]);
62+
}
63+
64+
public function testLogInterpolatesBooleanPlaceholder(): void
65+
{
66+
$this->output->expects($this->once())
67+
->method('writeln')
68+
->with(
69+
$this->stringContains('Active: 1'),
70+
$this->anything()
71+
);
72+
73+
$this->logger->info('Active: {active}', ['active' => true]);
74+
}
75+
76+
public function testLogInterpolatesObjectWithToString(): void
77+
{
78+
$object = new class {
79+
public function __toString(): string
80+
{
81+
return 'StringableObject';
82+
}
83+
};
84+
85+
$this->output->expects($this->once())
86+
->method('writeln')
87+
->with(
88+
$this->stringContains('Object: StringableObject'),
89+
$this->anything()
90+
);
91+
92+
$this->logger->info('Object: {obj}', ['obj' => $object]);
93+
}
94+
95+
public function testLogInterpolatesDateTimeInterface(): void
96+
{
97+
$date = new DateTime('2024-01-15T10:30:00+00:00');
98+
99+
$this->output->expects($this->once())
100+
->method('writeln')
101+
->with(
102+
$this->stringContains('Date: 2024-01-15T10:30:00+00:00'),
103+
$this->anything()
104+
);
105+
106+
$this->logger->info('Date: {date}', ['date' => $date]);
107+
}
108+
109+
public function testLogInterpolatesObjectWithoutToStringAsClassName(): void
110+
{
111+
$object = new \stdClass();
112+
113+
$this->output->expects($this->once())
114+
->method('writeln')
115+
->with(
116+
$this->stringContains('[object stdClass]'),
117+
$this->anything()
118+
);
119+
120+
$this->logger->info('Object: {obj}', ['obj' => $object]);
121+
}
122+
123+
public function testLogInterpolatesArrayAsArrayType(): void
124+
{
125+
$this->output->expects($this->once())
126+
->method('writeln')
127+
->with(
128+
$this->stringContains('[array]'),
129+
$this->anything()
130+
);
131+
132+
$this->logger->info('Data: {data}', ['data' => ['foo', 'bar']]);
133+
}
134+
135+
public function testLogInterpolatesNullAsEmpty(): void
136+
{
137+
$this->output->expects($this->once())
138+
->method('writeln')
139+
->with(
140+
$this->stringContains('Value: </info>'),
141+
$this->anything()
142+
);
143+
144+
$this->logger->info('Value: {value}', ['value' => null]);
145+
}
146+
147+
public function testLogWithoutPlaceholdersReturnsMessageUnchanged(): void
148+
{
149+
$this->output->expects($this->once())
150+
->method('writeln')
151+
->with(
152+
$this->stringContains('Simple message'),
153+
$this->anything()
154+
);
155+
156+
$this->logger->info('Simple message');
157+
}
158+
159+
public function testLogThrowsExceptionForInvalidLevel(): void
160+
{
161+
$this->expectException(InvalidArgumentException::class);
162+
$this->expectExceptionMessage('The log level "invalid" does not exist.');
163+
164+
$this->logger->log('invalid', 'test message');
165+
}
166+
167+
public function testHasErroredReturnsFalseInitially(): void
168+
{
169+
$this->assertSame(false, $this->logger->hasErrored());
170+
}
171+
172+
public function testHasErroredReturnsTrueAfterErrorLog(): void
173+
{
174+
$this->logger->error('An error occurred');
175+
176+
$this->assertSame(true, $this->logger->hasErrored());
177+
}
178+
179+
public function testHasErroredReturnsTrueAfterCriticalLog(): void
180+
{
181+
$this->logger->critical('Critical error');
182+
183+
$this->assertSame(true, $this->logger->hasErrored());
184+
}
185+
186+
public function testHasErroredReturnsFalseAfterInfoLog(): void
187+
{
188+
$this->logger->info('Info message');
189+
190+
$this->assertSame(false, $this->logger->hasErrored());
191+
}
192+
193+
public function testErrorLevelWritesToErrorOutput(): void
194+
{
195+
$errorOutput = $this->createMock(OutputInterface::class);
196+
$errorOutput->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_NORMAL);
197+
$errorOutput->expects($this->once())
198+
->method('writeln')
199+
->with(
200+
$this->stringContains('error message'),
201+
$this->anything()
202+
);
203+
204+
$consoleOutput = $this->createMock(ConsoleOutputInterface::class);
205+
$consoleOutput->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_NORMAL);
206+
$consoleOutput->method('getErrorOutput')->willReturn($errorOutput);
207+
208+
$logger = new ConsoleLogger($consoleOutput);
209+
$logger->error('error message');
210+
}
211+
212+
public function testDebugLevelNotWrittenAtNormalVerbosity(): void
213+
{
214+
$this->output->expects($this->never())->method('writeln');
215+
216+
$this->logger->debug('debug message');
217+
}
218+
219+
public function testDebugLevelWrittenAtVerboseLevel(): void
220+
{
221+
$verboseOutput = $this->createMock(OutputInterface::class);
222+
$verboseOutput->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_VERBOSE);
223+
$verboseOutput->expects($this->once())
224+
->method('writeln')
225+
->with(
226+
$this->stringContains('debug message'),
227+
$this->anything()
228+
);
229+
230+
$logger = new ConsoleLogger($verboseOutput);
231+
$logger->debug('debug message');
232+
}
233+
234+
// Additional log level tests
235+
236+
public function testHasErroredReturnsTrueAfterEmergencyLog(): void
237+
{
238+
$this->logger->emergency('Emergency error');
239+
240+
$this->assertSame(true, $this->logger->hasErrored());
241+
}
242+
243+
public function testHasErroredReturnsTrueAfterAlertLog(): void
244+
{
245+
$this->logger->alert('Alert error');
246+
247+
$this->assertSame(true, $this->logger->hasErrored());
248+
}
249+
250+
public function testHasErroredReturnsFalseAfterWarningLog(): void
251+
{
252+
$this->logger->warning('Warning message');
253+
254+
$this->assertSame(false, $this->logger->hasErrored());
255+
}
256+
257+
public function testHasErroredReturnsFalseAfterNoticeLog(): void
258+
{
259+
$this->logger->notice('Notice message');
260+
261+
$this->assertSame(false, $this->logger->hasErrored());
262+
}
263+
264+
public function testCustomVerbosityLevelMapIsUsed(): void
265+
{
266+
// Create custom map where info requires verbose
267+
$customVerbosityMap = [
268+
LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE,
269+
];
270+
271+
$normalOutput = $this->createMock(OutputInterface::class);
272+
$normalOutput->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_NORMAL);
273+
$normalOutput->expects($this->never())->method('writeln');
274+
275+
$logger = new ConsoleLogger($normalOutput, $customVerbosityMap);
276+
$logger->info('This should not be written at normal verbosity');
277+
}
278+
279+
public function testCustomFormatLevelMapIsUsed(): void
280+
{
281+
// Create custom map where warning is treated as error (writes to error output)
282+
$customFormatMap = [
283+
LogLevel::WARNING => ConsoleLogger::ERROR,
284+
];
285+
286+
$errorOutput = $this->createMock(OutputInterface::class);
287+
$errorOutput->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_NORMAL);
288+
$errorOutput->expects($this->once())
289+
->method('writeln')
290+
->with(
291+
$this->stringContains('warning treated as error'),
292+
$this->anything()
293+
);
294+
295+
$consoleOutput = $this->createMock(ConsoleOutputInterface::class);
296+
$consoleOutput->method('getVerbosity')->willReturn(OutputInterface::VERBOSITY_NORMAL);
297+
$consoleOutput->method('getErrorOutput')->willReturn($errorOutput);
298+
299+
$logger = new ConsoleLogger($consoleOutput, [], $customFormatMap);
300+
$logger->warning('warning treated as error');
301+
302+
// Warning with custom format map should set errored flag
303+
$this->assertSame(true, $logger->hasErrored());
304+
}
305+
}

0 commit comments

Comments
 (0)