From 6ac4e60b71289c212d1cf80e22c5c2b500d16e5b Mon Sep 17 00:00:00 2001
From: Dyszczo <krzysztof.dyszczyk@gmail.com>
Date: Mon, 25 May 2020 09:47:39 +0000
Subject: [PATCH] Feature/init

---
 .gitattributes                      |  15 +++
 .gitignore                          |   5 +
 .gitlab-ci.yml                      |   9 ++
 README.md                           |  54 +++++++++
 changelog.txt                       |   1 +
 composer.json                       |  34 ++++++
 phpunit-integration.xml             |  28 +++++
 phpunit-unit.xml                    |  21 ++++
 src/GetStrategy.php                 |  44 +++++++
 src/PostTypeStrategy.php            |  39 +++++++
 src/ShouldShowStrategy.php          |  13 +++
 tests/docker-compose.yaml           | 172 ++++++++++++++++++++++++++++
 tests/integration/bootstrap.php     |  28 +++++
 tests/unit/Stub/WP_Post.php         |   6 +
 tests/unit/TestGetStrategy.php      |  83 ++++++++++++++
 tests/unit/TestPostTypeStrategy.php |  55 +++++++++
 tests/unit/bootstrap.php            |   9 ++
 17 files changed, 616 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore
 create mode 100644 .gitlab-ci.yml
 create mode 100644 changelog.txt
 create mode 100644 composer.json
 create mode 100644 phpunit-integration.xml
 create mode 100644 phpunit-unit.xml
 create mode 100644 src/GetStrategy.php
 create mode 100644 src/PostTypeStrategy.php
 create mode 100644 src/ShouldShowStrategy.php
 create mode 100644 tests/docker-compose.yaml
 create mode 100644 tests/integration/bootstrap.php
 create mode 100644 tests/unit/Stub/WP_Post.php
 create mode 100644 tests/unit/TestGetStrategy.php
 create mode 100644 tests/unit/TestPostTypeStrategy.php
 create mode 100644 tests/unit/bootstrap.php

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..a612c0c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+tests/ export-ignore
+vendor/ export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.git/ export-ignore
+.gitlab-ci.yml export-ignore
+.idea export-ignore
+apigen.neon export-ignore
+build-coverage/ export-ignore
+docs/ export-ignore
+LICENSE.md export-ignore
+phpcs.xml.dist export-ignore
+phpunit-integration.xml export-ignore
+phpunit-unit.xml export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4b86223
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/vendor/
+.idea
+composer.lock
+build-coverage
+swagger
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..38447e9
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,9 @@
+variables:
+  DISABLE_FUNCTIONAL: 1
+  DISABLE_ACCEPTANCE: 1
+  DISABLE_PHP_5_5: 1
+  DISABLE_CODECEPTION: 1
+  DISABLE_INTEGRATION_TESTS: 1
+  IS_LIBRARY: 1
+
+include: 'https://gitlab.com/wpdesk/gitlab-ci/raw/master/gitlab-ci-1.2.yml'
diff --git a/README.md b/README.md
index e69de29..2afdc49 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,54 @@
+[![pipeline status](https://gitlab.com/wpdesk/wp-show-decision/badges/master/pipeline.svg)](https://gitlab.com/wpdesk/wp-show-decision/pipelines) 
+[![coverage report](https://gitlab.com/wpdesk/wp-show-decision/badges/master/coverage.svg)](https://gitlab.com/wpdesk/wp-show-decision/commits/master) 
+[![Latest Stable Version](https://poser.pugx.org/wpdesk/wp-show-decision/v/stable)](https://packagist.org/packages/wpdesk/wp-show-decision) 
+[![Total Downloads](https://poser.pugx.org/wpdesk/wp-show-decision/downloads)](https://packagist.org/packages/wpdesk/wp-show-decision) 
+[![Latest Unstable Version](https://poser.pugx.org/wpdesk/wp-show-decision/v/unstable)](https://packagist.org/packages/wpdesk/wp-show-decision) 
+[![License](https://poser.pugx.org/wpdesk/wp-show-decision/license)](https://packagist.org/packages/wpdesk/wp-show-decision)
+
+WordPress Library for Decision whether to show something. 
+===================================================
+Idea is that you have ShouldShowStrategy interface with one bool method. Thanks to this you can create a class that
+delegates decision whether to show something. Your class does not have to hardcode that decision.
+
+## Requirements
+
+PHP 5.6 or later.
+
+## Composer
+
+You can install the bindings via [Composer](http://getcomposer.org/). Run the following command:
+
+```bash
+composer require --dev wpdesk/wp-show-decision
+```
+
+To use the bindings, use Composer's [autoload](https://getcomposer.org/doc/01-basic-usage.md#autoloading):
+
+```php
+require_once 'vendor/autoload.php';
+```
+
+## Usage
+
+```php
+    new Beacon(
+        '6057086f-4b25-4e12-8735-fbc556d2dc01',
+        new PostTypeStrategy('automation'),
+        $this->get_plugin_assets_url()
+```
+
+```php
+    if ( is_admin() ) {
+        ( new RateNotices(
+            [ new TwoWeeksNotice( $this->plugin_url . '/assets', new PostTypeStrategy([
+                [ 'page' => 'x', 'name' => 'stefan' ], // show when page = x and name = stefan
+                [ 'page' => 'y' ]  // OR when page = y
+            ] ) ) ]
+        ) )->hooks();
+    }
+```
+
+
+## Project documentation
+
+PHPDoc: https://wpdesk.gitlab.io/wp-show-decision/index.html  
\ No newline at end of file
diff --git a/changelog.txt b/changelog.txt
new file mode 100644
index 0000000..8531c03
--- /dev/null
+++ b/changelog.txt
@@ -0,0 +1 @@
+* First release
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..667851a
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,34 @@
+{
+  "name": "wpdesk/wp-show-decision",
+  "authors": [
+    {
+      "name": "Krzysiek",
+      "email": "krzysiek@wpdesk.pl"
+    }
+  ],
+  "require": {
+    "php": ">=5.6",
+    "ext-curl": "*",
+    "ext-json": "*"
+  },
+  "require-dev": {
+    "phpunit/phpunit": "<7",
+    "wp-coding-standards/wpcs": "^0.14.1",
+    "squizlabs/php_codesniffer": "^3.0.2",
+    "mockery/mockery": "*",
+    "10up/wp_mock": "*"
+  },
+  "autoload": {
+    "psr-4": {
+      "WPDesk\\ShowDecision\\": "src/"
+    }
+  },
+  "autoload-dev": {
+  },
+  "scripts": {
+    "phpunit-unit": "phpunit --configuration phpunit-unit.xml --coverage-text --colors=never",
+    "phpunit-unit-fast": "phpunit --configuration phpunit-unit.xml --no-coverage",
+    "phpunit-integration": "phpunit --configuration phpunit-integration.xml --coverage-text --colors=never",
+    "phpunit-integration-fast": "phpunit --configuration phpunit-integration.xml --no-coverage"
+  }
+}
diff --git a/phpunit-integration.xml b/phpunit-integration.xml
new file mode 100644
index 0000000..4a342ab
--- /dev/null
+++ b/phpunit-integration.xml
@@ -0,0 +1,28 @@
+<phpunit bootstrap="tests/integration/bootstrap.php"
+         backupGlobals="false"
+     >
+    <testsuites>
+        <testsuite>
+            <directory prefix="Test" suffix=".php">./tests/integration</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <whitelist>
+            <directory suffix=".php">src</directory>
+        </whitelist>
+    </filter>
+
+    <logging>
+        <log type="junit" target="build-coverage/report.junit.xml"/>
+        <log type="coverage-html" target="build-coverage/coverage" charset="UTF-8" yui="true" highlight="true"/>
+        <log type="coverage-text" target="build-coverage/coverage.txt"/>
+        <log type="coverage-clover" target="build-coverage/clover.xml"/>
+    </logging>
+
+    <php>
+        <env name="WP_DEVELOP_DIR" value="/tmp/wordpress-develop"/>
+        <env name="WC_DEVELOP_DIR" value="/tmp/woocommerce"/>
+    </php>
+
+</phpunit>
\ No newline at end of file
diff --git a/phpunit-unit.xml b/phpunit-unit.xml
new file mode 100644
index 0000000..31e5c9f
--- /dev/null
+++ b/phpunit-unit.xml
@@ -0,0 +1,21 @@
+<phpunit bootstrap="tests/unit/bootstrap.php">
+    <testsuites>
+        <testsuite>
+            <directory prefix="Test" suffix=".php">./tests/unit/</directory>
+        </testsuite>
+    </testsuites>
+
+    <filter>
+        <whitelist>
+            <directory suffix=".php">src</directory>
+        </whitelist>
+    </filter>
+
+    <logging>
+        <log type="junit" target="build-coverage/report.junit.xml"/>
+        <log type="coverage-html" target="build-coverage/coverage" charset="UTF-8" yui="true" highlight="true"/>
+        <log type="coverage-text" target="build-coverage/coverage.txt"/>
+        <log type="coverage-clover" target="build-coverage/clover.xml"/>
+    </logging>
+
+</phpunit>
diff --git a/src/GetStrategy.php b/src/GetStrategy.php
new file mode 100644
index 0000000..b22ac92
--- /dev/null
+++ b/src/GetStrategy.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace WPDesk\ShowDecision;
+
+/**
+ * Show when some conditions with $_GET are meet.
+ */
+class GetStrategy implements ShouldShowStrategy {
+	/**
+	 * @var array
+	 */
+	private $conditions;
+
+	/**
+	 * @param array $conditions Whether to show on the page or not. Array of arrays with condition for _GET.
+	 *
+	 * Inner arrays mean AND, outer arrays mean OR conditions.
+	 *
+	 * ie. [ [ .. and .. and ..] or [ .. and .. and ..] or .. ]
+	 *
+	 */
+	public function __construct( array $conditions ) {
+		$this->conditions = $conditions;
+	}
+
+	/**
+	 * @return bool
+	 */
+	public function shouldDisplay() {
+		foreach ( $this->conditions as $or_conditions ) {
+			$display = true;
+			foreach ( $or_conditions as $parameter => $value ) {
+				if ( ! isset( $_GET[ $parameter ] ) || $_GET[ $parameter ] !== $value ) {
+					$display = false;
+				}
+			}
+			if ( $display ) {
+				return $display;
+			}
+		}
+
+		return false;
+	}
+}
diff --git a/src/PostTypeStrategy.php b/src/PostTypeStrategy.php
new file mode 100644
index 0000000..fe768f1
--- /dev/null
+++ b/src/PostTypeStrategy.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace WPDesk\ShowDecision;
+
+/**
+ * Show when pages associated with given post type is displayed.
+ */
+class PostTypeStrategy implements ShouldShowStrategy {
+	/** @var string */
+	private $post_type;
+
+	public function __construct( $post_type ) {
+		$this->post_type = $post_type;
+	}
+
+	/**
+	 * Should Beacon be visible?
+	 *
+	 * @return bool
+	 */
+	public function shouldDisplay() {
+		return $this->is_current_post_type_automation() ||
+		       ( isset( $_GET['post_type'] ) && $_GET['post_type'] === $this->post_type );
+	}
+
+	/**
+	 * @return bool
+	 */
+	private function is_current_post_type_automation() {
+		if ( isset( $_GET['post'] ) ) {
+			$post = get_post( (int) $_GET['post'] );
+			if ( $post instanceof \WP_Post ) {
+				return $post->post_type === $this->post_type;
+			}
+		}
+
+		return false;
+	}
+}
diff --git a/src/ShouldShowStrategy.php b/src/ShouldShowStrategy.php
new file mode 100644
index 0000000..c957cd0
--- /dev/null
+++ b/src/ShouldShowStrategy.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace WPDesk\ShowDecision;
+
+/**
+ * Should something be shown?
+ */
+interface ShouldShowStrategy {
+	/**
+	 * @return bool
+	 */
+	public function shouldDisplay();
+}
diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml
new file mode 100644
index 0000000..2a86b03
--- /dev/null
+++ b/tests/docker-compose.yaml
@@ -0,0 +1,172 @@
+version: '2.0'
+
+services:
+
+  wordpress:
+    image: wpdesknet/phpunit-woocommerce:0-0
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql0
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql0
+
+  wordpress-0-1:
+    image: wpdesknet/phpunit-woocommerce:0-1
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql1
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql1
+
+  wordpress-0-2:
+    image: wpdesknet/phpunit-woocommerce:0-2
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql2
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql2
+
+  wordpress-0-3:
+    image: wpdesknet/phpunit-woocommerce:0-3
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql3
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql3
+
+  wordpress-0-4:
+    image: wpdesknet/phpunit-woocommerce:0-4
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql4
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql4
+
+  wordpress-0-5:
+    image: wpdesknet/phpunit-woocommerce:0-5
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql5
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql5
+
+  wordpress-1-0:
+    image: wpdesknet/phpunit-woocommerce:1-0
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql0
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql0
+
+  wordpress-2-0:
+    image: wpdesknet/phpunit-woocommerce:2-0
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql0
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql0
+
+  wordpress-3-0:
+    image: wpdesknet/phpunit-woocommerce:3-0
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql0
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql0
+
+  wordpress-4-0:
+    image: wpdesknet/phpunit-woocommerce:4-0
+    volumes:
+    - .././:/opt/project
+    depends_on:
+    - mysql0
+    environment:
+      WORDPRESS_DB_NAME: wptest
+      WORDPRESS_DB_USER: mysql
+      WORDPRESS_DB_PASSWORD: mysql
+      WORDPRESS_DB_HOST: mysql0
+
+  mysql0:
+    image: mysql:5.7
+    environment:
+      MYSQL_ROOT_PASSWORD: mysql
+      MYSQL_DATABASE: wptest
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+
+  mysql1:
+    image: mysql:5.7
+    environment:
+      MYSQL_ROOT_PASSWORD: mysql
+      MYSQL_DATABASE: wptest
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+
+  mysql2:
+    image: mysql:5.7
+    environment:
+      MYSQL_ROOT_PASSWORD: mysql
+      MYSQL_DATABASE: wptest
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+
+  mysql3:
+    image: mysql:5.7
+    environment:
+      MYSQL_ROOT_PASSWORD: mysql
+      MYSQL_DATABASE: wptest
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+
+  mysql4:
+    image: mysql:5.7
+    environment:
+      MYSQL_ROOT_PASSWORD: mysql
+      MYSQL_DATABASE: wptest
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+
+  mysql5:
+    image: mysql:5.7
+    environment:
+      MYSQL_ROOT_PASSWORD: mysql
+      MYSQL_DATABASE: wptest
+      MYSQL_USER: mysql
+      MYSQL_PASSWORD: mysql
+
diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php
new file mode 100644
index 0000000..a422fd9
--- /dev/null
+++ b/tests/integration/bootstrap.php
@@ -0,0 +1,28 @@
+<?php
+
+ini_set('error_reporting', E_ALL); // or error_reporting(E_ALL);
+ini_set('display_errors', '1');
+ini_set('display_startup_errors', '1');
+
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+// disable xdebug backtrace
+if ( function_exists( 'xdebug_disable' ) ) {
+	xdebug_disable();
+}
+
+if ( getenv( 'PLUGIN_PATH' ) !== false ) {
+	define( 'PLUGIN_PATH', getenv( 'PLUGIN_PATH' ) );
+} else {
+	define( 'PLUGIN_PATH', __DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR );
+}
+
+require_once( getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit/includes/functions.php' );
+
+tests_add_filter( 'muplugins_loaded', function () {
+}, 100 );
+
+putenv('WP_TESTS_DIR=' . getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit');
+require_once( getenv( 'WC_DEVELOP_DIR' ) . '/tests/bootstrap.php' );
+
+do_action('plugins_loaded');
\ No newline at end of file
diff --git a/tests/unit/Stub/WP_Post.php b/tests/unit/Stub/WP_Post.php
new file mode 100644
index 0000000..bba4f36
--- /dev/null
+++ b/tests/unit/Stub/WP_Post.php
@@ -0,0 +1,6 @@
+<?php
+
+class WP_Post {
+	public $ID;
+	public $post_type;
+}
\ No newline at end of file
diff --git a/tests/unit/TestGetStrategy.php b/tests/unit/TestGetStrategy.php
new file mode 100644
index 0000000..7da82b5
--- /dev/null
+++ b/tests/unit/TestGetStrategy.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace unit;
+
+use PHPUnit\Framework\TestCase;
+use WPDesk\ShowDecision\GetStrategy;
+
+class TestGetStrategy extends TestCase {
+	const true_1 = [ 'a' => '1' ];
+	const true_2 = [ 'b' => '2' ];
+	const true_3 = [ 'c' => '3' ];
+
+	const false_1 = [ 'x' => '1' ];
+	const false_2 = [ 'y' => '2' ];
+
+	/**
+	 * Prepares $_GET with true clauses
+	 */
+	private function prepare_get() {
+		$_GET = [
+			self::true_1,
+			self::true_2,
+			self::true_3
+		];
+	}
+
+	protected function setUp() {
+		parent::setUp();
+		$this->prepare_get();
+	}
+
+	public function testAndClausesFailure() {
+		$strategy = new GetStrategy( [
+			[
+				self::true_1,
+				self::false_1
+			]
+		] );
+		$this->assertFalse( $strategy->shouldDisplay() );
+	}
+
+	public function testAndClausesSuccess() {
+		$strategy = new GetStrategy( [
+			[
+				self::true_1,
+				self::true_2,
+				self::true_3
+			]
+		] );
+		$this->assertTrue( $strategy->shouldDisplay() );
+	}
+
+	public function testOrClausesSuccess() {
+		$strategy = new GetStrategy( [
+			[
+				self::true_1,
+				self::false_1
+			],
+			[
+				self::true_1,
+				self::true_2
+			]
+		] );
+		$this->assertTrue( $strategy->shouldDisplay() );
+	}
+
+	public function testOrClausesFailure() {
+		$strategy = new GetStrategy( [
+			[
+				self::true_1,
+				self::false_1
+			],
+			[
+				self::true_1,
+				self::true_2,
+				self::true_3,
+				self::false_1
+			]
+		] );
+		$this->assertFalse( $strategy->shouldDisplay() );
+	}
+
+}
diff --git a/tests/unit/TestPostTypeStrategy.php b/tests/unit/TestPostTypeStrategy.php
new file mode 100644
index 0000000..31eb4ca
--- /dev/null
+++ b/tests/unit/TestPostTypeStrategy.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace unit;
+
+use PHPUnit\Framework\TestCase;
+use WPDesk\ShowDecision\PostTypeStrategy;
+
+
+class TestPostTypeStrategy extends TestCase {
+	const valid_post_type = 'test_post_type';
+	const invalid_post_type = 'invalid';
+
+	protected function setUp() {
+		require_once __DIR__ . DIRECTORY_SEPARATOR . 'Stub' . DIRECTORY_SEPARATOR . 'WP_Post.php';
+		parent::setUp();
+		\WP_Mock::setUp();
+	}
+
+	protected function tearDown() {
+		parent::tearDown();
+		\WP_Mock::tearDown();
+	}
+
+	public function testFailureWhenTriesUseInvalidPostType() {
+		$strategy = new PostTypeStrategy( self::invalid_post_type );
+		$this->assertFalse( $strategy->shouldDisplay() );
+	}
+
+	public function testValidPostTypeUsingGetSuccess() {
+		$_GET     = [ 'post_type' => self::valid_post_type ];
+		$strategy = new PostTypeStrategy( self::valid_post_type );
+		$this->assertTrue( $strategy->shouldDisplay() );
+	}
+
+	public function testValidPostTypeUsingPostIdSuccess() {
+		$post_id = 123;
+		$_GET    = [
+			'post' => $post_id
+		];
+		\WP_Mock::userFunction( 'get_post', [
+			'times'  => 1,
+			'return' => function () use ( $post_id ) {
+				$post            = new \WP_Post();
+				$post->ID        = $post_id;
+				$post->post_type = self::valid_post_type;
+
+				return $post;
+			}
+		] );
+
+		$strategy = new PostTypeStrategy( self::valid_post_type );
+		$this->assertTrue( $strategy->shouldDisplay() );
+	}
+
+}
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
new file mode 100644
index 0000000..76b8109
--- /dev/null
+++ b/tests/unit/bootstrap.php
@@ -0,0 +1,9 @@
+<?php
+/**
+ * PHPUnit bootstrap file
+ */
+
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+WP_Mock::setUsePatchwork( true );
+WP_Mock::bootstrap();
-- 
GitLab