Skip to content

Commit 4fdff5b

Browse files
oliverkleeSam Tuke
authored andcommitted
[FEATURE] System tests for testing and dev (#247)
System tests for production will follow in a separate commit.
1 parent fb9dd98 commit 4fdff5b

File tree

5 files changed

+285
-0
lines changed

5 files changed

+285
-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: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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 Symfony 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+
* @see getBaseUrl
51+
*
52+
* @param string $environment
53+
*
54+
* @return void
55+
*
56+
* @throws \InvalidArgumentException
57+
* @throws \RuntimeException
58+
*/
59+
protected function startSymfonyServer(string $environment)
60+
{
61+
if (!\in_array($environment, self::$validEnvironments, true)) {
62+
throw new \InvalidArgumentException('"' . $environment . '" is not a valid environment.', 1516284149961);
63+
}
64+
if ($this->lockFileExists()) {
65+
throw new \RuntimeException(
66+
'The server lock file "' . static::$lockFileName . '" already exists. ' .
67+
'Most probably, a symfony server already is running. ' .
68+
'Please stop the symfony server or delete the lock file.',
69+
1516622609
70+
);
71+
}
72+
73+
$this->serverProcess = new Process(
74+
$this->getSymfonyServerStartCommand($environment),
75+
$this->getApplicationRoot()
76+
);
77+
$this->serverProcess->start();
78+
79+
$this->waitForServerLockFileToAppear();
80+
// Give the server some more time to initialize so it will accept connections.
81+
\usleep(75000);
82+
}
83+
84+
/**
85+
* @return bool
86+
*/
87+
private function lockFileExists(): bool
88+
{
89+
return \file_exists($this->getFullLockFilePath());
90+
}
91+
92+
/**
93+
* @return string the base URL (including protocol and port, but without the trailing slash)
94+
*/
95+
protected function getBaseUrl(): string
96+
{
97+
return 'http://' . \file_get_contents($this->getFullLockFilePath());
98+
}
99+
100+
/**
101+
* Waits for the server lock file to appear, and throws an exception if the file has not appeared after the
102+
* maximum wait time.
103+
*
104+
* If the file already exists, this method returns instantly.
105+
*
106+
* @return void
107+
*
108+
* @throws \RuntimeException
109+
*/
110+
private function waitForServerLockFileToAppear()
111+
{
112+
$currentWaitTime = 0;
113+
while (!$this->lockFileExists() && $currentWaitTime < self::$maximumWaitTimeForServerLockFile) {
114+
\usleep(self::$waitTimeBetweenServerCommands);
115+
$currentWaitTime += self::$waitTimeBetweenServerCommands;
116+
}
117+
118+
if (!$this->lockFileExists()) {
119+
throw new \RuntimeException(
120+
'There is no symfony server lock file "' . static::$lockFileName . '".',
121+
1516625236
122+
);
123+
}
124+
}
125+
126+
/**
127+
* @return string
128+
*/
129+
private function getFullLockFilePath(): string
130+
{
131+
return $this->getApplicationRoot() . '/' . static::$lockFileName;
132+
}
133+
134+
/**
135+
* @return void
136+
*/
137+
protected function stopSymfonyServer()
138+
{
139+
if ($this->lockFileExists()) {
140+
$server = new WebServer();
141+
$server->stop($this->getFullLockFilePath());
142+
}
143+
if ($this->serverProcess !== null && $this->serverProcess->isRunning()) {
144+
$this->serverProcess->stop();
145+
}
146+
}
147+
148+
/**
149+
* @param string $environment
150+
*
151+
* @return string
152+
*/
153+
private function getSymfonyServerStartCommand(string $environment): string
154+
{
155+
$documentRoot = $this->getApplicationRoot() . '/web/';
156+
$this->checkDocumentRoot($documentRoot);
157+
158+
return sprintf(
159+
'%1$s server:start -d %2$s --env=%3$s',
160+
$this->getApplicationRoot() . '/bin/console',
161+
$documentRoot,
162+
$environment
163+
);
164+
}
165+
166+
/**
167+
* @return string
168+
*/
169+
protected function getApplicationRoot(): string
170+
{
171+
if (self::$applicationStructure === null) {
172+
self::$applicationStructure = new ApplicationStructure();
173+
}
174+
175+
return self::$applicationStructure->getApplicationRoot();
176+
}
177+
178+
/**
179+
* Checks that $documentRoot exists, is a directory and readable.
180+
*
181+
* @param string $documentRoot
182+
*
183+
* @return void
184+
*
185+
* @throws \RuntimeException
186+
*/
187+
private function checkDocumentRoot(string $documentRoot)
188+
{
189+
if (!\file_exists($documentRoot)) {
190+
throw new \RuntimeException('The document root "' . $documentRoot . '" does not exist.', 1499513246550);
191+
}
192+
if (!\is_dir($documentRoot)) {
193+
throw new \RuntimeException(
194+
'The document root "' . $documentRoot . '" exists, but is no directory.',
195+
1499513263757
196+
);
197+
}
198+
if (!\is_readable($documentRoot)) {
199+
throw new \RuntimeException(
200+
'The document root "' . $documentRoot . '" exists and is a directory, but is not readable.',
201+
1499513279590
202+
);
203+
}
204+
}
205+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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::assertSame(200, $response->getStatusCode());
58+
}
59+
60+
/**
61+
* @test
62+
* @param string $environment
63+
* @dataProvider environmentDataProvider
64+
*/
65+
public function homepageReturnsDummyContent(string $environment)
66+
{
67+
$this->startSymfonyServer($environment);
68+
69+
$response = $this->httpClient->request('GET', '/', ['base_uri' => $this->getBaseUrl()]);
70+
71+
self::assertContains('This page has been intentionally left empty.', $response->getBody()->getContents());
72+
}
73+
}

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)