diff --git a/content/posts/2015-01-24-multiple-return-values-with-mockery.md b/content/posts/2015-01-24-multiple-return-values-with-mockery.md new file mode 100644 index 0000000..e1dc285 --- /dev/null +++ b/content/posts/2015-01-24-multiple-return-values-with-mockery.md @@ -0,0 +1,105 @@ ++++ +title = Mockery: returning values and throwing exceptions +date = 2015-04-24 +author = Jacob Kiers ++++ + +Last week, I had to write a piece of code that contains retry logic. Naturally, I want to test it. That proved trickier than expected. + +The application code looks like this: + +```php +class Sender +{ + protected $connection; + + public function send() + { + $success = false; + + $i = 0; + do { + $i++; + + try { + $success = $this->doSend($i); + } catch (SenderException $e) { + $success = false; + } + + } while (!$success && $i < 3); + + return $success; + } + + protected function doSend($data) + { + // Can throw SenderException + $response = $this->connection->send($data); + + if ('OK' === $response) { + return true; + } + + return false; + } +} +``` + +I specifically want to test the retry logic, so I have to mock the ::doSend() method. Then I can simulate the different outcomes (returning true or false, or throwing a SenderException). + +I use [Mockery] to do the real work. It is a great library. If you don't know it yet, please check it out. I will wait right here... + +Now, since ::doSend() is a protected method, Mockery must be instructed to allow that. + +So after the first try, I ended up with: + +```php +public function testItWillRetrySending() +{ + $sender = M::mock('Sender'); + $sender->shouldAllowMockingProtectedMethods() + + $sender->shouldReceive('doSend') + ->andReturn(false, new Exception()); +} +``` + +To my surprise, this did not work. Instead of throwing the exception, Mockery returns it. So my next try was this: + +```php +$sender->shouldReceive('doSend') + ->andReturn(false) + ->andThrow(new Exception()); +``` + +Another surprise: with this code, Mockery will always throw the exception, and ignore the first return value (false). After some debugging, I found out that Mockery just overwrites the return values in this case. + +Fortunately, there is another way to return multiple return values: the ::andReturnUsing() method. It gives full control over the return values. + +So I ended up with this testing code: + +```php +$return_value_generator = function () { + static $counter = 0; + + $counter++; + + switch ($counter) { + case 1: return false; + case 2: throw new SenderException(); + case 3: return true; + default: throw new Exception("Should never reach this."); + } +}; + +$sender = M::mock('Sender'); + +$sender->shouldAllowMockingProtectedMethods() + ->shouldReceive('doSend') + ->andReturnUsing($return_value_generator); +``` + +This works perfectly. It feels a bit like a hack though. So if you know a better way or have any other remarks, please let me know. + +[Mockery]: https://github.com/mockery/mockery \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..42499d1 --- /dev/null +++ b/index.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/publish-zones.php b/publish-zones.php new file mode 100644 index 0000000..e68679e --- /dev/null +++ b/publish-zones.php @@ -0,0 +1,172 @@ +fileName = basename(dirname($file)).'/'.basename($file); + $this->data = file_get_contents($file); + $this->hash = md5($this->data); + + $this->parseMetadata(); + } + + public function toRecords(): string + { + $chunks = str_split(base64_encode($this->data), 250); + + $records = []; + $records[] = "; {$this->getDnsName()}"; + $records[] = $this->getDnsName().$this->buildMiddle().'"'.$this->buildIndexString(count($chunks)).'"'; + + foreach($chunks as $id => $chunk) + { + $records[] = $this->buildRecord($id, $chunk); + } + + $result = ""; + foreach ($records as $record) + { + $result .= $record . PHP_EOL; + } + + return $result; + } + + private function getDnsName(): string + { + return str_replace(['/', '\\', '.'], '-', $this->fileName); + } + + private function buildIndexString(int $chunkCount): string + { + $metadata = base64_encode(json_encode($this->metadata)); + return "t={$this->mimeType()};c={$chunkCount};h={$this->hash};m={$metadata}"; + } + + private function buildMiddle(): string + { + return "\t".self::TTL."\tIN\tTXT\t"; + } + + private function buildRecord(int $id, string $data): string + { + $name = "$id.{$this->hash}"; + + return "$name".$this->buildMiddle().'"'.$data.'"'; + } + + private function mimeType() + { + $extension = pathinfo($this->fileName, PATHINFO_EXTENSION); + + switch($extension) + { + case "js": + return "text/javascript"; + case "md": + return "text/markdown"; + + default: + return mime_content_type($this->file); + } + } + + private function parseMetadata() + { + if ($this->mimeType() != "text/markdown") return []; + + $frontmatter_chars = "+++"; + + $end_of_frontmatter = strpos($this->data, $frontmatter_chars, 2); + if (false === $end_of_frontmatter) { + var_dump($end_of_frontmatter); + return []; + } + + $frontmatter = substr( + $this->data, + strlen($frontmatter_chars) + 1, + $end_of_frontmatter - strlen($frontmatter_chars) - 2 + ); + + $this->data = trim(substr($this->data, $end_of_frontmatter + strlen($frontmatter_chars))).PHP_EOL; + + $lines = explode(PHP_EOL, $frontmatter); + foreach($lines as $line) { + $eq_pos = strpos($line, "="); + $key = trim(substr($line, 0, $eq_pos - 1)); + $value = trim(substr($line, $eq_pos + 1)); + $this->metadata[$key] = $value; + } + } +} + +function update_serial_line(string $line) +{ + $matches = []; + preg_match("#(\s+)(\d{10})(.*)#", $line, $matches); + list($_, $start, $serial, $last) = $matches; + + $today = date("Ymd"); + $day = substr($serial, 0, 8); + $nr = (int) substr($serial, -2); + + if ($day === $today) { + $nr++; + } else { + $day = $today; + $nr = 1; + } + + $new_serial = sprintf('%1$s%2$02d', $day, $nr); + + return $start.$new_serial.$last; +} + +$zone_file = "zones/db.hod.experiments.jacobkiers.net"; +if (!file_exists($zone_file)) { + fwrite(STDERR, "The zone file {$zone_file} does not exist!"); + exit(1); +} + +$lines = file($zone_file); + +foreach($lines as $index => &$line) +{ + if (str_contains($line, "; serial")) { + $matches = []; + preg_match("#(\s+)(\d{10})(.*)#", $line, $matches); + list($_, $start, $serial, $last) = $matches; + $line = update_serial_line($line).PHP_EOL; + } + if (str_starts_with($line, ";; START BLOG RECORDS")) break; +} + +$zone_file_contents = implode("", array_slice($lines, 0, $index+1)); + +echo $zone_file_contents; + +$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__."/content")); +foreach ($it as $file) +{ + if ($file->isDir()) continue; + if (str_contains($file->getPathname(), "ignore")) continue; + + $bootstrap = new Content($file->getPathname()); + $zone_file_contents .= PHP_EOL.$bootstrap->toRecords().PHP_EOL; +} + +$zone_file_contents .= PHP_EOL; + +#echo $zone_file_contents; + +file_put_contents($zone_file, $zone_file_contents); \ No newline at end of file diff --git a/zones/db.hod.experiments.jacobkiers.net b/zones/db.hod.experiments.jacobkiers.net new file mode 100644 index 0000000..8318285 --- /dev/null +++ b/zones/db.hod.experiments.jacobkiers.net @@ -0,0 +1,19 @@ +; +$TTL 5m ; Default TTL +@ IN SOA home.kie.rs. postmaster.kie.rs. ( + 2021081611 ; serial + 1h ; slave refresh interval + 15m ; slave retry interval + 1w ; slave copy expire time + 1h ; NXDOMAIN cache time + ) + +$ORIGIN hod.experiments.jacobkiers.net. + +; +; domain name servers +; +@ IN NS home.kie.rs. + + +;; START BLOG RECORDS