Skip to content

Commit ef52fa8

Browse files
author
Oliver Klee
committed
[FEATURE] System tests for testing and dev
System tests for production will follow in a separate commit.
1 parent 67260a8 commit ef52fa8

File tree

5 files changed

+289
-0
lines changed

5 files changed

+289
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*.bak
33
/.idea/
44
/.project
5+
/.web-server-pid
56
/.webprj
67
/composer.lock
78
/Configuration/bundles.yml

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ script:
7272
echo "Running the integration tests";
7373
vendor/bin/phpunit -c Configuration/PHPUnit/phpunit.xml Tests/Integration/;
7474
75+
- >
76+
echo;
77+
echo "Running the system tests";
78+
vendor/bin/phpunit -c Configuration/PHPUnit/phpunit.xml Tests/System/;
79+
7580
- >
7681
echo;
7782
echo "Running the static analysis";
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace PhpList\PhpList4\TestingSupport\Traits;
5+
6+
use PhpList\PhpList4\Core\ApplicationStructure;
7+
use Symfony\Bundle\WebServerBundle\WebServer;
8+
use Symfony\Component\Process\Process;
9+
10+
/**
11+
* Trait for running the PHP server in the background.
12+
*
13+
* @author Oliver Klee <oliver@phplist.com>
14+
*/
15+
trait SymfonyServerTrait
16+
{
17+
/**
18+
* @var Process
19+
*/
20+
private $serverProcess = null;
21+
22+
/**
23+
* @var string[]
24+
*/
25+
private static $validEnvironments = ['test', 'dev', 'prod'];
26+
27+
/**
28+
* @var string
29+
*/
30+
private static $lockFileName = '.web-server-pid';
31+
32+
/**
33+
* @var int microseconds
34+
*/
35+
private static $maximumWaitTimeForServerLockFile = 5000000;
36+
37+
/**
38+
* @var int microseconds
39+
*/
40+
private static $waitTimeBetweenServerCommands = 50000;
41+
42+
/**
43+
* @var ApplicationStructure
44+
*/
45+
private static $applicationStructure = null;
46+
47+
/**
48+
* Starts the symfony server. The resulting base URL then can be retrieved using getBaseUrl().
49+
*
50+
* Note: It might still take some time for the server lock file to appear.
51+
*
52+
* @see getBaseUrl
53+
*
54+
* @param string $environment
55+
*
56+
* @return void
57+
*
58+
* @throws \InvalidArgumentException
59+
* @throws \RuntimeException
60+
*/
61+
protected function startSymfonyServer(string $environment)
62+
{
63+
if (!\in_array($environment, self::$validEnvironments, true)) {
64+
throw new \InvalidArgumentException('"' . $environment . '" is not a valid environment.', 1516284149961);
65+
}
66+
if ($this->lockFileExists()) {
67+
throw new \RuntimeException(
68+
'The server lock file "' . static::$lockFileName . '" already exists. ' .
69+
'Most probably, a symfony server already is running. ' .
70+
'Please stop the symfony server or delete the lock file.',
71+
1516622609
72+
);
73+
}
74+
75+
$this->serverProcess = new Process(
76+
$this->getSymfonyServerStartCommand($environment),
77+
$this->getApplicationRoot()
78+
);
79+
$this->serverProcess->start();
80+
81+
$this->waitForServerLockFileToAppear();
82+
// Give the server some more time to initialize so it will accept connections.
83+
\usleep(50000);
84+
}
85+
86+
/**
87+
* @return bool
88+
*/
89+
private function lockFileExists(): bool
90+
{
91+
return \file_exists($this->getFullLockFilePath());
92+
}
93+
94+
/**
95+
* @return string the base URL (including protocol and port, but without the trailing slash)
96+
*/
97+
protected function getBaseUrl(): string
98+
{
99+
return 'http://' . file_get_contents($this->getFullLockFilePath());
100+
}
101+
102+
/**
103+
* Waits for the server lock file to appear, and throws an exception if the file has not appeared after the
104+
* maximum wait time.
105+
*
106+
* If the file already exists, this method returns instantly.
107+
*
108+
* @return void
109+
*
110+
* @throws \RuntimeException
111+
*/
112+
private function waitForServerLockFileToAppear()
113+
{
114+
$currentWaitTime = 0;
115+
while (!$this->lockFileExists() && $currentWaitTime < self::$maximumWaitTimeForServerLockFile) {
116+
\usleep(self::$waitTimeBetweenServerCommands);
117+
$currentWaitTime += self::$waitTimeBetweenServerCommands;
118+
}
119+
120+
if (!$this->lockFileExists()) {
121+
throw new \RuntimeException(
122+
'There is no symfony server lock file "' . static::$lockFileName . '".',
123+
1516625236
124+
);
125+
}
126+
}
127+
128+
/**
129+
* @return string
130+
*/
131+
private function getFullLockFilePath(): string
132+
{
133+
return $this->getApplicationRoot() . '/' . static::$lockFileName;
134+
}
135+
136+
/**
137+
* @return void
138+
*/
139+
protected function stopSymfonyServer()
140+
{
141+
if ($this->lockFileExists()) {
142+
$server = new WebServer();
143+
$server->stop($this->getFullLockFilePath());
144+
}
145+
if ($this->serverProcess !== null && $this->serverProcess->isRunning()) {
146+
$this->serverProcess->stop();
147+
}
148+
}
149+
150+
/**
151+
* @param string $environment
152+
*
153+
* @return string
154+
*/
155+
private function getSymfonyServerStartCommand(string $environment): string
156+
{
157+
$documentRoot = $this->getApplicationRoot() . '/web/';
158+
$this->checkDocumentRoot($documentRoot);
159+
160+
return sprintf(
161+
'%1$s server:start -d %2$s --env=%3$s',
162+
$this->getApplicationRoot() . '/bin/console',
163+
$documentRoot,
164+
$environment
165+
);
166+
}
167+
168+
/**
169+
* @return string
170+
*/
171+
protected function getApplicationRoot(): string
172+
{
173+
if (self::$applicationStructure === null) {
174+
self::$applicationStructure = new ApplicationStructure();
175+
}
176+
177+
return self::$applicationStructure->getApplicationRoot();
178+
}
179+
180+
/**
181+
* Checks that $documentRoot exists, is a directory and readable.
182+
*
183+
* @param string $documentRoot
184+
*
185+
* @return void
186+
*
187+
* @throws \RuntimeException
188+
*/
189+
private function checkDocumentRoot(string $documentRoot)
190+
{
191+
if (!file_exists($documentRoot)) {
192+
throw new \RuntimeException('The document root "' . $documentRoot . '" does not exist.', 1499513246550);
193+
}
194+
if (!is_dir($documentRoot)) {
195+
throw new \RuntimeException(
196+
'The document root "' . $documentRoot . '" exists, but is no directory.',
197+
1499513263757
198+
);
199+
}
200+
if (!is_readable($documentRoot)) {
201+
throw new \RuntimeException(
202+
'The document root "' . $documentRoot . '" exists and is a directory, but is not readable.',
203+
1499513279590
204+
);
205+
}
206+
}
207+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace PhpList\PhpList4\Tests\System\ApplicationBundle;
5+
6+
use GuzzleHttp\Client;
7+
use PhpList\PhpList4\TestingSupport\Traits\SymfonyServerTrait;
8+
use PHPUnit\Framework\TestCase;
9+
use Psr\Http\Message\ResponseInterface;
10+
11+
/**
12+
* Testcase.
13+
*
14+
* @author Oliver Klee <oliver@phplist.com>
15+
*/
16+
class PhpListApplicationBundleTest extends TestCase
17+
{
18+
use SymfonyServerTrait;
19+
20+
/**
21+
* @var Client
22+
*/
23+
private $httpClient = null;
24+
25+
protected function setUp()
26+
{
27+
$this->httpClient = new Client();
28+
}
29+
30+
protected function tearDown()
31+
{
32+
$this->stopSymfonyServer();
33+
}
34+
35+
/**
36+
* @return string[][]
37+
*/
38+
public function environmentDataProvider(): array
39+
{
40+
return [
41+
'test' => ['test'],
42+
'dev' => ['dev'],
43+
];
44+
}
45+
46+
/**
47+
* @test
48+
* @param string $environment
49+
* @dataProvider environmentDataProvider
50+
*/
51+
public function homepageReturnsSuccess(string $environment)
52+
{
53+
$this->startSymfonyServer($environment);
54+
55+
$response = $this->httpClient->request('GET', '/', ['base_uri' => $this->getBaseUrl()]);
56+
57+
self::assertInstanceOf(ResponseInterface::class, $response);
58+
self::assertSame(200, $response->getStatusCode());
59+
}
60+
61+
/**
62+
* @test
63+
* @param string $environment
64+
* @dataProvider environmentDataProvider
65+
*/
66+
public function homepageReturnsDummyContent(string $environment)
67+
{
68+
$this->startSymfonyServer($environment);
69+
70+
$response = $this->httpClient->request('GET', '/', ['base_uri' => $this->getBaseUrl()]);
71+
72+
self::assertInstanceOf(ResponseInterface::class, $response);
73+
self::assertContains('This page has been intentionally left empty.', $response->getBody()->getContents());
74+
}
75+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"require-dev": {
4444
"phpunit/phpunit": "^6.5.0",
4545
"phpunit/dbunit": "^3.0.0",
46+
"guzzlehttp/guzzle": "^6.3.0",
4647
"squizlabs/php_codesniffer": "^3.2.0",
4748
"phpstan/phpstan": "^0.7.0",
4849
"nette/caching": "^2.5.0 || ^3.0.0",

0 commit comments

Comments
 (0)