Phpunit test no exception

2. Writing Tests for PHPUnit¶

Example 2.1 shows how we can write tests using PHPUnit that exercise PHP’s array operations. The example introduces the basic conventions and steps for writing tests with PHPUnit:

The tests for a class Class go into a class ClassTest .

ClassTest inherits (most of the time) from PHPUnit\Framework\TestCase .

The tests are public methods that are named test* .

Alternatively, you can use the @test annotation in a method’s docblock to mark it as a test method.

Inside the test methods, assertion methods such as assertSame() (see Assertions ) are used to assert that an actual value matches an expected value.

Test Dependencies¶

Adrian Kuhn et. al.:

Unit Tests are primarily written as a good practice to help developers identify and fix bugs, to refactor code and to serve as documentation for a unit of software under test. To achieve these benefits, unit tests ideally should cover all the possible paths in a program. One unit test usually covers one specific path in one function or method. However a test method is not necessarily an encapsulated, independent entity. Often there are implicit dependencies between test methods, hidden in the implementation scenario of a test.

PHPUnit supports the declaration of explicit dependencies between test methods. Such dependencies do not define the order in which the test methods are to be executed but they allow the returning of an instance of the test fixture by a producer and passing it to the dependent consumers.

  • A producer is a test method that yields its unit under test as return value.
  • A consumer is a test method that depends on one or more producers and their return values.

Example 2.2 shows how to use the @depends annotation to express dependencies between test methods.

In the example above, the first test, testEmpty() , creates a new array and asserts that it is empty. The test then returns the fixture as its result. The second test, testPush() , depends on testEmpty() and is passed the result of that depended-upon test as its argument. Finally, testPop() depends upon testPush() .

The return value yielded by a producer is passed “as-is” to its consumers by default. This means that when a producer returns an object, a reference to that object is passed to the consumers. Instead of a reference either (a) a (deep) copy via @depends clone , or (b) a (normal shallow) clone (based on PHP keyword clone ) via @depends shallowClone are possible too.

To localize defects, we want our attention to be focussed on relevant failing tests. This is why PHPUnit skips the execution of a test when a depended-upon test has failed. This improves defect localization by exploiting the dependencies between tests as shown in Example 2.3 .

A test may have more than one @depends annotation. PHPUnit does not change the order in which tests are executed, you have to ensure that the dependencies of a test can actually be met before the test is run.

A test that has more than one @depends annotation will get a fixture from the first producer as the first argument, a fixture from the second producer as the second argument, and so on. See Example 2.4

Data Providers¶

A test method can accept arbitrary arguments. These arguments are to be provided by one or more data provider methods ( additionProvider() in Example 2.5 ). The data provider method to be used is specified using the @dataProvider annotation.

A data provider method must be public and either return an array of arrays or an object that implements the Iterator interface and yields an array for each iteration step. For each array that is part of the collection the test method will be called with the contents of the array as its arguments.

When using a large number of datasets it’s useful to name each one with string key instead of default numeric. Output will be more verbose as it’ll contain that name of a dataset that breaks a test.

You can make the test output more verbose by defining a sentence and using the test’s parameter names as placeholders ( $a , $b and $expected in the example above) with the @testdox annotation. You can also refer to the name of a named data set with $_dataName .

Читайте также:  Vpn l2tp error 789

When a test receives input from both a @dataProvider method and from one or more tests it @depends on, the arguments from the data provider will come before the ones from depended-upon tests. The arguments from depended-upon tests will be the same for each data set. See Example 2.9

When a test depends on a test that uses data providers, the depending test will be executed when the test it depends upon is successful for at least one data set. The result of a test that uses data providers cannot be injected into a depending test.

All data providers are executed before both the call to the setUpBeforeClass() static method and the first call to the setUp() method. Because of that you can’t access any variables you create there from within a data provider. This is required in order for PHPUnit to be able to compute the total number of tests.

Testing Exceptions¶

Example 2.11 shows how to use the expectException() method to test whether an exception is thrown by the code under test.

In addition to the expectException() method the expectExceptionCode() , expectExceptionMessage() , and expectExceptionMessageMatches() methods exist to set up expectations for exceptions raised by the code under test.

Note that expectExceptionMessage() asserts that the $actual message contains the $expected message and does not perform an exact string comparison.

Testing PHP Errors, Warnings, and Notices¶

By default, PHPUnit converts PHP errors, warnings, and notices that are triggered during the execution of a test to an exception. Among other benefits, this makes it possible to expect that a PHP error, warning, or notice is triggered in a test as shown in Example 2.12 .

PHP’s error_reporting runtime configuration can limit which errors PHPUnit will convert to exceptions. If you are having issues with this feature, be sure PHP is not configured to suppress the type of error you are interested in.

When testing code that uses PHP built-in functions such as fopen() that may trigger errors it can sometimes be useful to use error suppression while testing. This allows you to check the return values by suppressing notices that would lead to an exception raised by PHPUnit’s error handler.

Without the error suppression the test would fail reporting fopen(/is-not-writeable/file): failed to open stream: No such file or directory .

Testing Output¶

Sometimes you want to assert that the execution of a method, for instance, generates an expected output (via echo or print , for example). The PHPUnit\Framework\TestCase class uses PHP’s Output Buffering feature to provide the functionality that is necessary for this.

Example 2.14 shows how to use the expectOutputString() method to set the expected output. If this expected output is not generated, the test will be counted as a failure.

Table 2.1 shows the methods provided for testing output

Table 2.1 Methods for testing output В¶

Method Meaning
void expectOutputRegex(string $regularExpression) Set up the expectation that the output matches a $regularExpression .
void expectOutputString(string $expectedString) Set up the expectation that the output is equal to an $expectedString .
bool setOutputCallback(callable $callback) Sets up a callback that is used to, for instance, normalize the actual output.
string getActualOutput() Get the actual output.

A test that emits output will fail in strict mode.

Error output¶

Whenever a test fails PHPUnit tries its best to provide you with as much context as possible that can help to identify the problem.

In this example only one of the array values differs and the other values are shown to provide context on where the error occurred.

When the generated output would be long to read PHPUnit will split it up and provide a few lines of context around every difference.

Edge Cases¶

When a comparison fails PHPUnit creates textual representations of the input values and compares those. Due to that implementation a diff might show more problems than actually exist.

This only happens when using assertEquals() or other ‘weak’ comparison functions on arrays or objects.

In this example the difference in the first index between 1 and ‘1’ is reported even though assertEquals() considers the values as a match.

© Copyright 2023, Sebastian Bergmann. Revision ea37ef9d .



Last week i gave 10 phpunit tips. This week we’ll take a look at testing exceptions, which wasn’t covered in that post.

Lets start with some example code that we will be testing. We have the Email and EmailValidator classes. Email is a value object that makes sure it is a valid email. We use the EmailValidator to make sure that the emails are only from our company.

Now, we create a test that makes sure we don’t allow emails other than . We use the expectException method to tell PHPUnit that we expect this exception. If it is not thrown, or if another exception is thrown, then this test will fail.

However, we made an error in this test. By starting with the expectException , the test will pass as long the InvalidArgumentException is thrown anywhere within this test. So if we make a typo, and pass foo@barcom to the Email::create , an exception will be thrown. And the test will pass. But then we never test the EmailValidator.

To make sure we are properly testing the exception we need to call expectException just before the method that throws. Now if we run the test, it still passes. But if we make a mistake, and Email::create throws an exception, our test will fail.

Testing multiple exception paths

Your method may throw exceptions for different reasons. For example, lets say our email validator also makes sure that our colleague bob can’t log in. So the method would look like this.

Now we need to test the second path. So lets write that.

But we forgot to turn into . The test is still green, but we don’t actually test that bob can’t log in.

There are a few options here. We could introduce different exception classes for each error state. Or we can use expectExceptionMessage to make sure we get the correct error. So if we add $this->expectExceptionMessage(‘bob is no longer allowed to log in’); just after the expectException call, then our test turns red. If we then change to , our test turns green again.

The best practice would be to do both. Introducing new exception classes per error means we can more easily catch the right error. However, you may be dealing with legacy code, where a new exception class could break things. Validating you get the right error message is really important if that message faces users. But even if it is only for other devs, you want to make sure you get the correct exception.

Testing Exceptions with data

Sometimes we use exceptions to pass data back. Perhaps a validator that can have multiple failures, and that has an array of all the errors. Lets take a look at the FormValidator class, and how we would test it. This class validates some information, and may throw an exception, containing the data of all things that went wrong.

If we need to test this, using expectException doesn’t make much sense. expectExceptionMessage wont help us out either, as it doesn’t have a message. Instead we can use the good only try catch here.

Important here is the fail after the validation. If the method throws no exception, we need to fail the test. If we don’t add that call then our test would pass if no exception was thrown.

But, if you don’t need to check any details, other than the exception class, message or code, use expectException .

In conclusion

You can use expectException to test your exceptions. But only set the exception just before it is thrown. Use expectExceptionMessage if the message is important, or if it is the only way to see where something went wrong. Use try catch if you need to validate specific properties of the exception.

For this post, PHPUnit 9.5 and PHP 8.0 were used. If you want to run the tests for yourself, you can find them on github. If you were looking for information on how to test warnings and notices, i have a post about that as well right here.

Gert de Pagter
Software Engineer

My interests include software development, math and magic.


2. Написание тестов на PHPUnit

Пример 2.1 показывает, как мы можем писать тесты, используя PHPUnit, которые выполняют операции с массивом PHP. В этом примере представлены основные соглашения и шаги для написания тестов с помощью PHPUnit:

Тесты для класса Class содержатся в классе ClassTest .

ClassTest наследуется (чаще всего) от PHPUnit\Framework\TestCase .

Тесты — общедоступные методы с именами test* .

Кроме того, вы можете использовать аннотацию @test в докблоке метода, чтобы пометить его как метод тестирования.

Внутри тестовых методов для проверки того, соответствует ли фактическое значение ожидаемому используются методы-утверждения, такие как assertSame() (см. Утверждения ).

Зависимости тестов

Адриан Кун (Adrian Kuhn) и другие:

Модульные тесты главным образом пишутся в качестве хорошей практики, помогающей разработчикам выявлять и исправлять баги, проводить рефакторинг кода и служить в качестве документации для тестируемого программного модуля (программы). Для достижения этих преимуществ модульные тесты в идеале должны охватывать все возможные пути исполнения программе. Один модульный тест обычно покрывает один конкретный путь в одной функции или метода. Однако тестовые методы необязательно должны быть инкапсулированными и независимыми. Часто существуют неявные зависимости между тестовыми методами, скрытые в сценарии реализации теста.

PHPUnit поддерживает объявление явных зависимостей между тестовыми методами. Эти зависимости не определяют порядок, в котором должны выполняться тестовые методы, но они позволяют возвращать экземпляр (данные) фикстуры теста, созданные поставщиком (producer) для передачи его зависимым потребителям (consumers).

  • Поставщик — тестовый метод, который предоставляет свой тестируемый модуль в качестве возвращаемого значения.
  • Потребитель — тестовый метод, который зависит от одного или более поставщиков и их возвращаемых значений.

Пример 2.2 показывает, как использовать аннотацию @depends для представления зависимостей между тестовыми методами.

В вышеприведённом примере первый тест, testEmpty() , создаёт новый массив и утверждает, что он пуст. Затем тест возвращает фикстуру в качестве результата. Второй тест, testPush() , зависит от testEmpty() и ему передаётся результат этого зависимого теста в качестве аргумента. Наконец, testPop() зависит от testPush() .

Возвращаемое значение, предоставленное поставщиком, по умолчанию передаётся потребителям «как есть». Это означает, что когда поставщик возвращает объект, ссылка на этот объект передаётся потребителям. Вместо ссылки возможна, либо (а) (глубокая) копия через @depends clone или (б) (поверхностная) копия (на основе ключевого слова PHP clone ) через @depends shallowClone .

Чтобы быстро находить дефекты, нам нужно сосредоточить внимание на соответствующих неудачных тестах. Вот почему PHPUnit пропускает выполнение теста, когда зависимый тест (тест с зависимостью) провалился (не прошёл). Это помогает локализовать дефекты за счёт использования зависимостей между тестами, как это показано в Пример 2.3 .

У теста может быть несколько аннотаций @depends . PHPUnit не изменяет порядок выполнения тестов, поэтому вы должны убедиться, что все зависимости действительно могут быть выполнены до запуска теста.

Тест, содержащий более одной аннотации @depends , получит фикстуру от первого поставщика в качестве первого аргумента, фикстуру от второго поставщика вторым аргументом и т.д. См. Пример 2.4

Провайдеры данных

Тестовый метод может принимать произвольное количество аргументов. Эти аргументы могут быть предоставлены одним или несколькими методами провайдеров данных (data provider) (см. additionProvider() в Пример 2.5 ). Метод, который будет использован в качестве провайдера данных, обозначается с помощью аннотации @dataProvider .

Метод провайдера данных должен быть объявлен как public и возвращать либо массив массивов, либо объект, реализующий интерфейс Iterator и возвращать массив при каждой итерации. Для каждого массива, являющегося частью коллекции, будет вызываться тестовый метод с элементами массива в качестве его аргументов.

При использовании большого количества наборов данных полезно указывать для каждого из них строковый ключ, вместо использования числового ключа по умолчанию. Вывод станет более подробным, так как он будет содержать имя набора данных, не прошедший тест.


Оцените статью