Let's talk about PHP code refactoring_PHP tutorial
WBOY
Release: 2016-07-13 10:37:24
Original
1090 people have browsed it
As PHP has evolved from a simple scripting language to a full-fledged programming language, the code base of a typical PHP application has grown in complexity. To control the support and maintenance of these applications, we can use various testing tools to automate the process. One of these is unit testing, which allows you to directly test the correctness of the code you write. However, often legacy code bases are not suitable for this kind of testing. This article will describe refactoring strategies for PHP code that contain common issues in order to simplify the process of testing with popular unit testing tools while reducing dependencies on improving the code base.
Introduction
Looking back on the 15 years of development of PHP, we find that it has transformed from a simple dynamic scripting language used to replace the popular CGI scripts at the time into a mature modern programming language. As the code base grows, manual testing has become an impossible task, and all code changes, no matter how big or small, have an impact on the entire application. These impacts may be as small as just affecting the loading of a certain page or saving a form, or they may produce problems that are difficult to detect, or they may produce errors that only occur under specific conditions. Even, it may cause previously fixed issues to reappear in the application. Many testing tools have been developed to solve these problems.
One of the popular methods is so-called functional or acceptance testing, which tests the application through its typical user interactions. This is a great way to test individual processes in an application, but the testing process can be very slow and generally fails to test whether the underlying classes and methods are working as expected. At this time, we need to use another testing method, which is unit testing. The goal of unit testing is to test the functionality of the underlying code of the application and ensure that they produce correct results after execution. Often, these "growing" web applications slowly introduce legacy code that becomes increasingly difficult to test over time, making it difficult for the development team to ensure application test coverage. This is often called "untestable code". Now let's look at how to identify untestable code in your application, and ways to fix it.
Identify untestable code
The problem domain regarding the untestability of a code base is often not obvious when writing the code. When writing PHP application code, people tend to write code following the flow of web requests, which usually results in a more process-oriented approach to application design. The rush to complete a project or quickly fix an application can prompt developers to "cut corners" in order to get coding done quickly. Previously, poorly written or confusing code could exacerbate untestability issues in an application because developers would often make the least risky fix, even if it might create subsequent support issues. These problem areas cannot be discovered through normal unit testing.
Functions that depend on global state
Global variables are convenient in PHP applications. They allow you to initialize some variables or objects in your application and then use them elsewhere in the application. However, this flexibility comes at a price, and overuse of global variables is a common problem with untestable code. We can see this happening in Listing 1.
Listing 1. Functions that depend on global state
function formatNumber($number)
{
global $decimal_precision, $decimal_separator, $thousands_separator;
if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ',';
These global variables bring two different problems. The first problem is that you need to consider all of these global variables in your tests, ensuring that they are set to valid values that are acceptable to the function. The second and more serious problem is that you cannot modify the state of subsequent tests and invalidate their results, you need to ensure that the global state is reset to the state before the test was run. PHPUnit has tools that can help you back up global variables and restore their values after a test has run, which can help with this problem. However, a better approach is to enable the test class to pass the values of these global variables directly to methods. Listing 2 shows an example of this approach.
Listing 2. Modify this function to support overriding global variables
function formatNumber($number, $decimal_precision = null, $decimal_separator = null,
$thousands_separator = null)
{
if ( is_null($decimal_precision) ) global $decimal_precision;
if ( is_null($decimal_separator) ) global $decimal_separator;
if ( is_null($thousands_separator) ) global $thousands_separator;
if ( !isset($decimal_precision) ) $decimal_precision = 2;
if ( !isset($decimal_separator) ) $decimal_separator = '.';
if ( !isset($thousands_separator) ) $thousands_separator = ',';
Not only does this make the code more testable, but it also makes it less dependent on the method's global variables. This allows us to refactor the code to no longer use global variables.
Single instance that cannot be reset
Singleton refers to a class that is designed to allow only one instance to exist in an application at a time. They are a common pattern used in applications for global objects, such as database connections and configuration settings. They are often considered a no-no in applications because many developers see little use in creating an object that is always available, so they don't pay much attention to it. This problem mainly stems from the overuse of singletons, which results in a large number of so-called god objects that are not scalable. But the biggest problem from a testing perspective is that they are usually unchangeable. Listing 3 is an example of this.
Listing 3. The Singleton object we want to test
class Singleton
{
private static $instance;
protected function __construct() { }
private final function __clone() {}
public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
} }
return self::$instance;
}
}
You can see that after a single instance is instantiated for the first time, each call to the getInstance() method actually returns the same object. It will not create a new object. If we modify this object, then It may cause serious problems. The simplest solution is to add a reset method to the object. Listing 4 shows an example of this.
Listing 4. Singleton object with reset method added
class Singleton
{
private static $instance;
protected function __construct() { }
private final function __clone() {}
public static function getInstance()
{
if ( !isset(self::$instance) ) {
self::$instance = new Singleton;
} }
return self::$instance;
}
public static function reset()
{
self::$instance = null;
}
}
Now, we can call the reset method before each test to ensure that we will execute the initialization code of the singleton object first during each test. All in all, adding this method to our application is useful because we can now easily modify a single instance.
Use class constructor
A good practice for unit testing is to only test the code that needs to be tested and avoid creating unnecessary objects and variables. Every object and variable you create needs to be deleted after testing. This becomes a problem for troublesome projects such as files and database tables, because in these cases if you need to modify state, then you have to be more careful about doing some cleanup after the test is complete. The biggest obstacle to adhering to this rule is the constructor of the object itself, which performs all operations that are irrelevant to the test. Listing 5 is an example of this.
Listing 5. Class with a large singleton method
class MyClass
{
protected $results;
public function __construct()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
}
public function getFirstResult()
{
return $this->results[0];
}
}
Here, in order to test the fdfdfd method of the object, we ultimately need to establish a database connection, add some records to the table, and then clear all these resources after testing. If testing fdfdfd does not require these things at all, then this process may be too complicated. Therefore, we modify the constructor shown in Listing 6.
Listing 6. Class modified to ignore all unnecessary initialization logic
class MyClass
{
protected $results;
public function __construct($init = true)
{
if ( $init ) $this->init();
}
public function init()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$this->results = $dbconn->query('select name from mytable');
}
public function getFirstResult()
{
return $this->results[0];
}
}
We refactored a large amount of code in the constructor and moved them to an init() method, which will still be called by the constructor by default to avoid breaking the logic of the existing code. However, now we can only pass a boolean value false to the constructor during testing to avoid calling the init() method and all unnecessary initialization logic. This refactoring of the class also improves the code because we separate the initialization logic from the object's constructor.
Hardcoded class dependencies
As we introduced in the previous section, a large number of class design issues that make testing difficult focus on initializing various objects that do not need to be tested. Earlier, we knew that heavy initialization logic may cause a great burden on test writing (especially when the test does not require these objects at all), but if we create these objects directly in the class method of the test, it may cause Another question. Listing 7 shows sample code that might cause this problem.
Listing 7. Directly initializing another object’s class in a method
class MyUserClass
{
public function getUserList()
{
$dbconn = new DatabaseConnection('localhost','user','password');
$results = $dbconn->query('select name from user');
sort($results);
return $results;
}
}
Suppose we are testing the getUserList method above, but our test focus is to ensure that the returned user list is correctly sorted alphabetically. In this case, our question is not whether we can get these records from the database, because what we want to test is whether we can sort the returned records. The problem is, since we instantiate a database connection object directly in this method, we need to perform all these tedious operations to complete the test of the method. Therefore, we need to modify the method so that this object can be inserted in the middle, as shown in Listing 8.
Listing 8. This class has a method that directly instantiates another object, but also provides an overridden method
$dbconn = new DatabaseConnection('localhost','user','password');
} }
$results = $dbconn->query('select name from user');
sort($results);
return $results;
}
}
Now you can directly pass in an object that is compatible with the expected database connection object and use this object directly instead of creating a new object. You can also pass in a mock object, that is, we can directly return the value we want in some calling methods in a hard-coded way. Here, we can simulate the query method of the database connection object, so that we only need to return the results without actually querying the database. Refactoring like this can also improve this approach because it allows your application to plug in different database connections when needed, rather than just binding to a designated default database connection.
Benefits of testable code
Obviously, writing more testable code can certainly simplify unit testing of PHP applications (as you can see in the examples shown in this article), but in the process, it can also improve the application's performance. Design, modularity and stability. We've all seen "spaghetti" code, which is stuffed with a lot of business and presentation logic in one of the main processes of a PHP application, which will undoubtedly cause serious support problems for those using the application. In the process of making the code more testable, we refactored some of the previous problematic code; these codes were not only problematic in design, but also functional. We improve the reusability of our code by making these functions and classes more versatile, and by removing hard-coded dependencies, making them more easily reusable by other parts of the application. Additionally, we are replacing poorly written code with better quality code to simplify future support of the code base.
Conclusion
In this article, we learned how to improve the testability of PHP code through some typical examples of untestable code in PHP applications. We also describe how these situations can arise in applications, and then how to appropriately fix the problematic code to facilitate testing. We also learned that these code modifications can not only improve the testability of the code, but also generally improve the quality of the code and improve the reusability of the refactored code.
http://www.bkjia.com/PHPjc/735874.htmlwww.bkjia.comtruehttp: //www.bkjia.com/PHPjc/735874.htmlTechArticleAs PHP transforms from a simple scripting language to a mature programming language, a typical PHP application The complexity of the program's code base also increases. In order to control the use of these applications...
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn