diff --git a/README.md b/README.md
index 31a2fe6..45b0439 100644
--- a/README.md
+++ b/README.md
@@ -57,14 +57,14 @@ working follow the steps below:
   "Oauth access token" and the "Oauth access token secret"
 
 ### Storing field values
-If you want to store the fields that are retrived from Twitter you should create
+If you want to store the fields that are retrieved from Twitter you should create
 appropriate fields on the created media bundle (image, content and
 retweet_count) and map this to the fields provided by Twitter.php.
 
-**NOTE:** At the momemnt there is no GUI for that, so the only method of doing
+**NOTE:** At the moment there is no GUI for that, so the only method of doing
 that for now is via CMI.
 
-This whould be an example of that (the field_map section):
+This would be an example of that (the field_map section):
 
 ```
 langcode: en
diff --git a/config/schema/media_entity_twitter.schema.yml b/config/schema/media_entity_twitter.schema.yml
index 90c364b..04c37e6 100644
--- a/config/schema/media_entity_twitter.schema.yml
+++ b/config/schema/media_entity_twitter.schema.yml
@@ -4,7 +4,7 @@ media_entity_twitter.settings:
   mapping:
     local_images:
       type: string
-      label: 'Base folder for tuhmbnails'
+      label: 'Base folder for thumbnails'
 
 media_entity.bundle.type.twitter:
   type: mapping
@@ -28,3 +28,6 @@ media_entity.bundle.type.twitter:
     oauth_access_token_secret:
       type: string
       label: 'Field with Oauth access token secret'
+    generate_thumbnails:
+      type: boolean
+      label: 'Enable automatic thumbnail generation'
diff --git a/media_entity_twitter.module b/media_entity_twitter.module
index 363d96b..7985c2d 100644
--- a/media_entity_twitter.module
+++ b/media_entity_twitter.module
@@ -13,5 +13,49 @@ function media_entity_twitter_theme() {
     'media_entity_twitter_tweet' => [
       'variables' => ['path' => NULL, 'attributes' => []],
     ],
+    'media_entity_twitter_tweet_thumbnail' => [
+      'variables' => [
+        'content' => '',
+        'author' => '',
+        'avatar' => NULL,
+      ],
+    ],
   ];
 }
+
+/**
+ * Preprocess function for media_entity_twitter_tweet_thumbnail theme hook.
+ *
+ * @param array $variables
+ *   Variables to be injected into the template.
+ */
+function media_entity_twitter_preprocess_media_entity_twitter_tweet_thumbnail(array &$variables) {
+  // If the avatar exists, load it directly into memory and base64 encode it.
+  // For security reasons, browsers don't always allow external images xlinked
+  // in an SVG to be displayed when the SVG is being embedded via an <img> tag.
+  // The workaround is to embed the image directly into the SVG as a base64
+  // encoded string.
+  if ($variables['avatar']) {
+    $extension = pathinfo($variables['avatar'], PATHINFO_EXTENSION);
+    $extension = strtolower($extension);
+
+    // Don't fetch the avatar if it has an unrecognized extension.
+    if (in_array($extension, ['gif', 'jpg', 'jpeg', 'png', 'webp'])) {
+      $data = file_get_contents($variables['avatar']);
+
+      if ($data) {
+        // image/jpg is not a thing.
+        if ($extension == 'jpg') {
+          $extension = 'jpeg';
+        }
+        $variables['avatar'] = 'data:image/' . $extension . ';base64,' . base64_encode($data);
+      }
+      else {
+        unset($variables['avatar']);
+      }
+    }
+    else {
+      unset($variables['avatar']);
+    }
+  }
+}
diff --git a/media_entity_twitter.services.yml b/media_entity_twitter.services.yml
new file mode 100644
index 0000000..2391921
--- /dev/null
+++ b/media_entity_twitter.services.yml
@@ -0,0 +1,13 @@
+services:
+  media_entity_twitter.tweet_fetcher:
+    class: '\Drupal\media_entity_twitter\TweetFetcher'
+    arguments:
+      - '@media_entity_twitter.tweet_cache'
+
+  media_entity_twitter.tweet_cache:
+    class: '\Drupal\Core\Cache\CacheBackendInterface'
+    tags:
+      - { name: cache.bin, default_backend: cache.backend.chainedfast }
+    factory: cache_factory:get
+    arguments:
+      - tweet_cache
diff --git a/src/Plugin/MediaEntity/Type/Twitter.php b/src/Plugin/MediaEntity/Type/Twitter.php
index 18fa450..e2991f9 100644
--- a/src/Plugin/MediaEntity/Type/Twitter.php
+++ b/src/Plugin/MediaEntity/Type/Twitter.php
@@ -6,10 +6,11 @@ use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\media_entity\MediaInterface;
 use Drupal\media_entity\MediaTypeBase;
 use Drupal\media_entity\MediaTypeException;
-use Drupal\Component\Serialization\Json;
+use Drupal\media_entity_twitter\TweetFetcherInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -31,6 +32,20 @@ class Twitter extends MediaTypeBase {
   protected $configFactory;
 
   /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * The tweet fetcher.
+   *
+   * @var \Drupal\media_entity_twitter\TweetFetcherInterface
+   */
+  protected $tweetFetcher;
+
+  /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
@@ -40,7 +55,9 @@ class Twitter extends MediaTypeBase {
       $plugin_definition,
       $container->get('entity_type.manager'),
       $container->get('entity_field.manager'),
-      $container->get('config.factory')
+      $container->get('config.factory'),
+      $container->get('renderer'),
+      $container->get('media_entity_twitter.tweet_fetcher')
     );
   }
 
@@ -68,10 +85,16 @@ class Twitter extends MediaTypeBase {
    *   Entity field manager service.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   Config factory service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   * @param \Drupal\media_entity_twitter\TweetFetcherInterface $tweet_fetcher
+   *   The tweet fetcher.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, RendererInterface $renderer, TweetFetcherInterface $tweet_fetcher) {
     parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $config_factory->get('media_entity.settings'));
     $this->configFactory = $config_factory;
+    $this->renderer = $renderer;
+    $this->tweetFetcher = $tweet_fetcher;
   }
 
   /**
@@ -80,6 +103,7 @@ class Twitter extends MediaTypeBase {
   public function defaultConfiguration() {
     return [
       'use_twitter_api' => FALSE,
+      'generate_thumbnails' => FALSE,
     ];
   }
 
@@ -138,21 +162,23 @@ class Twitter extends MediaTypeBase {
           return FALSE;
 
         case 'image_local':
-          if (isset($tweet['extended_entities']['media'][0]['media_url'])) {
-            $local_uri = $this->configFactory->get('media_entity_twitter.settings')->get('local_images') . '/' . $matches['id'] . '.' . pathinfo($tweet['extended_entities']['media'][0]['media_url'], PATHINFO_EXTENSION);
+          $local_uri = $this->getField($media, 'image_local_uri');
 
-            if (!file_exists($local_uri)) {
-              file_prepare_directory($this->configFactory->get('media_entity_twitter.settings')->get('local_images'), FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
-              file_unmanaged_save_data($tweet['extended_entities']['media'][0]['media_url'], $local_uri, FILE_EXISTS_REPLACE);
+          if ($local_uri) {
+            if (file_exists($local_uri)) {
+              return $local_uri;
+            }
+            else {
+              $image_url = $this->getField($media, 'image');
+              return file_unmanaged_save_data($image_url, $local_uri, FILE_EXISTS_REPLACE);
             }
-
-            return $local_uri;
           }
           return FALSE;
 
         case 'image_local_uri':
-          if (isset($tweet['extended_entities']['media'][0]['media_url'])) {
-            return $this->configFactory->get('media_entity_twitter.settings')->get('local_images') . '/' . $matches['id'] . '.' . pathinfo($tweet['extended_entities']['media'][0]['media_url'], PATHINFO_EXTENSION);
+          $image_url = $this->getField($media, 'image');
+          if ($image_url) {
+            return $this->getLocalImageUri($matches['id'], $image_url);
           }
           return FALSE;
 
@@ -257,6 +283,22 @@ class Twitter extends MediaTypeBase {
       ),
     );
 
+    $form['generate_thumbnails'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Generate thumbnails'),
+      '#default_value' => $this->configuration['generate_thumbnails'],
+      '#states' => [
+        'visible' => [
+          ':input[name="type_configuration[twitter][use_twitter_api]"]' => [
+            'checked' => TRUE,
+          ],
+        ],
+      ],
+      '#description' => $this->t('If checked, Drupal will automatically generate thumbnails for tweets that do not reference any external media. In certain circumstances, <strong>this may violate <a href="@policy">Twitter\'s fair use policy</a></strong>. Please <strong>read it and be careful</strong> if you choose to enable this.', [
+        '@policy' => 'https://dev.twitter.com/overview/terms/agreement-and-policy',
+      ]),
+    ];
+
     return $form;
   }
 
@@ -280,6 +322,40 @@ class Twitter extends MediaTypeBase {
   }
 
   /**
+   * Computes the destination URI for a tweet image.
+   *
+   * @param mixed $id
+   *   The tweet ID.
+   * @param string|null $media_url
+   *   The URL of the media (i.e., photo, video, etc.) associated with the
+   *   tweet.
+   *
+   * @return string
+   *   The desired local URI.
+   */
+  protected function getLocalImageUri($id, $media_url = NULL) {
+    $directory = $this->configFactory
+      ->get('media_entity_twitter.settings')
+      ->get('local_images');
+
+    // Ensure the destination directory is writable. (TODO: Throw an exception
+    // or otherwise take action if it isn't.)
+    file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+
+    $local_uri = $directory . '/' . $id . '.';
+    if ($media_url) {
+      $local_uri .= pathinfo($media_url, PATHINFO_EXTENSION);
+    }
+    else {
+      // If there is no media associated with the tweet, we will generate an
+      // SVG thumbnail.
+      $local_uri .= 'svg';
+    }
+
+    return $local_uri;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getDefaultThumbnail() {
@@ -290,11 +366,35 @@ class Twitter extends MediaTypeBase {
    * {@inheritdoc}
    */
   public function thumbnail(MediaInterface $media) {
+    // If there's already a local image, use it.
     if ($local_image = $this->getField($media, 'image_local')) {
       return $local_image;
     }
 
-    return $this->getDefaultThumbnail();
+    // If thumbnail generation is disabled, use the default thumbnail.
+    if (empty($this->configuration['generate_thumbnails'])) {
+      return $this->getDefaultThumbnail();
+    }
+
+    // We might need to generate a thumbnail...
+    $id = $this->getField($media, 'id');
+    $thumbnail_uri = $this->getLocalImageUri($id);
+
+    // ...unless we already have, in which case, use it.
+    if (file_exists($thumbnail_uri)) {
+      return $thumbnail_uri;
+    }
+
+    // Render the thumbnail SVG using the theme system.
+    $thumbnail = [
+      '#theme' => 'media_entity_twitter_tweet_thumbnail',
+      '#content' => $this->getField($media, 'content'),
+      '#author' => $this->getField($media, 'user'),
+      '#avatar' => $this->getField($media, 'profile_image_url_https'),
+    ];
+    $svg = $this->renderer->renderRoot($thumbnail);
+
+    return file_unmanaged_save_data($svg, $thumbnail_uri, FILE_EXISTS_ERROR) ?: $this->getDefaultThumbnail();
   }
 
   /**
@@ -327,60 +427,24 @@ class Twitter extends MediaTypeBase {
   }
 
   /**
-   * Get auth settings.
-   *
-   * @return array
-   *   Array of auth settings.
-   */
-  protected function getAuthSettings() {
-    return array(
-      'consumer_key' => $this->configuration['consumer_key'],
-      'consumer_secret' => $this->configuration['consumer_secret'],
-      'oauth_access_token' => $this->configuration['oauth_access_token'],
-      'oauth_access_token_secret' => $this->configuration['oauth_access_token_secret'],
-    );
-  }
-
-  /**
    * Get a single tweet.
    *
    * @param int $id
    *   The tweet id.
    */
   protected function fetchTweet($id) {
-    $tweet = &drupal_static(__FUNCTION__);
-
-    if (!isset($tweet)) {
-      // Check for dependencies.
-      // @todo There is perhaps a better way to do that.
-      if (!class_exists('\TwitterAPIExchange')) {
-        drupal_set_message($this->t('Twitter library is not available. Consult the README.md for installation instructions.'), 'error');
-        return;
-      }
-
-      // Settings.
-      $auth_settings = $this->getAuthSettings();
-      $request_settings = array(
-        'url' => 'https://api.twitter.com/1.1/statuses/show.json',
-        'method' => 'GET',
-      );
-      $query = "?id=$id";
-
-      // Get the tweet.
-      $twitter = new \TwitterAPIExchange($auth_settings);
-      $result = $twitter->setGetfield($query)
-        ->buildOauth($request_settings['url'], $request_settings['method'])
-        ->performRequest();
+    $this->tweetFetcher->setCredentials(
+      $this->configuration['consumer_key'],
+      $this->configuration['consumer_secret'],
+      $this->configuration['oauth_access_token'],
+      $this->configuration['oauth_access_token_secret']
+    );
 
-      if ($result) {
-        return Json::decode($result);
-      }
-      else {
-        throw new MediaTypeException(NULL, 'The tweet could not be retrived.');
-      }
+    try {
+      return $this->tweetFetcher->fetchTweet($id);
     }
-    else {
-      return $tweet;
+    catch (\Exception $e) {
+      throw new MediaTypeException(NULL, $e->getMessage());
     }
   }
 
diff --git a/src/TweetFetcher.php b/src/TweetFetcher.php
new file mode 100644
index 0000000..d2c8cb2
--- /dev/null
+++ b/src/TweetFetcher.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace Drupal\media_entity_twitter;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\CacheBackendInterface;
+
+class TweetFetcher implements TweetFetcherInterface {
+
+  /**
+   * The optional cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * The current set of Twitter API credentials.
+   *
+   * @var array
+   */
+  protected $credentials = [];
+
+  /**
+   * The current API exchange object.
+   *
+   * @var \TwitterAPIExchange
+   */
+  protected $twitter;
+
+  /**
+   * TweetFetcher constructor.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface|NULL $cache
+   *   (optional) A cache bin for storing fetched tweets.
+   */
+  public function __construct(CacheBackendInterface $cache = NULL) {
+    $this->cache = $cache;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fetchTweet($id) {
+    // Tweets don't change much, so pull it out of the cache (if we have one)
+    // if this one has already been fetched.
+    if ($this->cache && $cached_tweet = $this->cache->get($id)) {
+      return $cached_tweet->data;
+    }
+
+    // Assume credentials have been set and query Twitter's API.
+    $response = $this->twitter
+      ->setGetfield('?id=' . $id)
+      ->buildOAuth('https://api.twitter.com/1.1/statuses/show.json', 'GET')
+      ->performRequest();
+
+    if (empty($response)) {
+      throw new \Exception('Could not retrieve tweet ' . $id);
+    }
+    // TODO: Check for errors in the response and throw errors if there are any.
+
+    $response = Json::decode($response);
+    // If we have a cache, store the response for future use.
+    if ($this->cache) {
+      $this->cache->set($id, $response);
+    }
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCredentials() {
+    return $this->credentials;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCredentials($consumer_key, $consumer_secret, $oauth_access_token, $oauth_access_token_secret) {
+    $this->credentials = [
+      'consumer_key' => $consumer_key,
+      'consumer_secret' => $consumer_secret,
+      'oauth_access_token' => $oauth_access_token,
+      'oauth_access_token_secret' => $oauth_access_token_secret,
+    ];
+    $this->twitter = new \TwitterAPIExchange($this->credentials);
+  }
+
+}
diff --git a/src/TweetFetcherInterface.php b/src/TweetFetcherInterface.php
new file mode 100644
index 0000000..6ac0d32
--- /dev/null
+++ b/src/TweetFetcherInterface.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\media_entity_twitter;
+
+/**
+ * Defines a wrapper around the Twitter API.
+ */
+interface TweetFetcherInterface {
+
+  /**
+   * Retrieves a tweet by its ID.
+   *
+   * @param int $id
+   *   The tweet ID.
+
+   * @return array
+   *   The tweet information.
+   *
+   * @throws \Exception
+   *   If the tweet could not be retrieved.
+   */
+  public function fetchTweet($id);
+
+  /**
+   * Returns the current Twitter API credentials.
+   *
+   * @return array
+   *   The API credentials. Will be an array with consumer_key, consumer_secret,
+   *   oauth_access_token, and oauth_access_token_secret elements.
+   */
+  public function getCredentials();
+
+  /**
+   * Sets the credentials for accessing Twitter's API.
+   *
+   * @param string $consumer_key
+   *   The consumer key.
+   * @param $consumer_secret
+   *   The consumer secret.
+   * @param string $oauth_access_token
+   *   The OAuth access token.
+   * @param string $oauth_access_token_secret
+   *   The OAuth access token secret.
+   */
+  public function setCredentials($consumer_key, $consumer_secret, $oauth_access_token, $oauth_access_token_secret);
+
+}
diff --git a/templates/media-entity-twitter-tweet-thumbnail.html.twig b/templates/media-entity-twitter-tweet-thumbnail.html.twig
new file mode 100644
index 0000000..0a2aa4f
--- /dev/null
+++ b/templates/media-entity-twitter-tweet-thumbnail.html.twig
@@ -0,0 +1,23 @@
+<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <rect width="100%" height="100%" fill="none" stroke="grey" rx="5%" ry="5%" />
+
+    {% if avatar %}
+    <image xlink:href="{{ avatar }}" width="25%" height="25%" x="5%" y="5%" />
+    {% endif %}
+
+    {% if author %}
+    <text font-family="Arial" fill="grey" font-size="32" x="32%" y="20%">@{{ author }}</text>
+    {% endif %}
+
+    {% if content %}
+    <foreignObject x="5%" y="35%" width="90%" height="60%">
+        <body xmlns="http://www.w3.org/1999/xhtml">
+        <p style="font-family: Arial; font-size: 200%; line-height: 150%;">{{ content }}</p>
+        </body>
+    </foreignObject>
+    {% endif %}
+
+    <g transform="translate(-450,-450)">
+        <path style="fill:#1da1f2; fill-opacity: .25; fill-rule:nonzero; stroke:none" d="m 633.89823,812.04479 c 112.46038,0 173.95627,-93.16765 173.95627,-173.95625 0,-2.64628 -0.0539,-5.28062 -0.1726,-7.90305 11.93799,-8.63016 22.31446,-19.39999 30.49762,-31.65984 -10.95459,4.86937 -22.74358,8.14741 -35.11071,9.62551 12.62341,-7.56929 22.31446,-19.54304 26.88583,-33.81739 -11.81284,7.00307 -24.89517,12.09297 -38.82383,14.84055 -11.15723,-11.88436 -27.04079,-19.31655 -44.62892,-19.31655 -33.76374,0 -61.14426,27.38052 -61.14426,61.13233 0,4.79784 0.5364,9.46458 1.58538,13.94057 -50.81546,-2.55686 -95.87353,-26.88582 -126.02546,-63.87991 -5.25082,9.03545 -8.27852,19.53111 -8.27852,30.73006 0,21.21186 10.79366,39.93837 27.20766,50.89296 -10.03077,-0.30992 -19.45363,-3.06348 -27.69044,-7.64676 -0.009,0.25652 -0.009,0.50661 -0.009,0.78077 0,29.60957 21.07478,54.3319 49.0513,59.93435 -5.13757,1.40062 -10.54335,2.15158 -16.12196,2.15158 -3.93364,0 -7.76596,-0.38716 -11.49099,-1.1026 7.78383,24.2932 30.35457,41.97073 57.11525,42.46543 -20.92578,16.40207 -47.28712,26.17062 -75.93712,26.17062 -4.92898,0 -9.79834,-0.28036 -14.58427,-0.84634 27.05868,17.34379 59.18936,27.46396 93.72193,27.46396"></path>
+    </g>
+</svg>
diff --git a/tests/src/Kernel/ThumbnailTest.php b/tests/src/Kernel/ThumbnailTest.php
new file mode 100644
index 0000000..d12b2d9
--- /dev/null
+++ b/tests/src/Kernel/ThumbnailTest.php
@@ -0,0 +1,177 @@
+<?php
+
+namespace Drupal\Tests\media_entity_twitter\Kernel;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\media_entity\Entity\Media;
+use Drupal\media_entity\Entity\MediaBundle;
+use Drupal\media_entity\MediaInterface;
+use Drupal\media_entity_twitter\Plugin\MediaEntity\Type\Twitter;
+use Drupal\media_entity_twitter\TweetFetcherInterface;
+
+/**
+ * @group media_entity_twitter
+ */
+class ThumbnailTest extends KernelTestBase {
+
+  /**
+   * The mocked tweet fetcher.
+   *
+   * @var TweetFetcherInterface
+   */
+  protected $tweetFetcher;
+
+  /**
+   * The plugin under test.
+   *
+   * @var Twitter
+   */
+  protected $plugin;
+
+  /**
+   * A tweet media entity.
+   *
+   * @var MediaInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'field',
+    'file',
+    'image',
+    'media_entity',
+    'media_entity_twitter',
+    'system',
+    'text',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('file');
+    $this->installEntitySchema('media');
+    $this->installConfig(['media_entity_twitter', 'system']);
+
+    $this->tweetFetcher = $this->getMock(TweetFetcherInterface::class);
+
+    MediaBundle::create([
+      'id' => 'tweet',
+      'type' => 'twitter',
+      'type_configuration' => [
+        'source_field' => 'tweet',
+        'use_twitter_api' => TRUE,
+        'consumer_key' => $this->randomString(),
+        'consumer_secret' => $this->randomString(),
+        'oauth_access_token' => $this->randomString(),
+        'oauth_access_token_secret' => $this->randomString(),
+      ],
+    ])->save();
+
+    FieldStorageConfig::create([
+      'field_name' => 'tweet',
+      'entity_type' => 'media',
+      'type' => 'string_long',
+    ])->save();
+
+    FieldConfig::create([
+      'field_name' => 'tweet',
+      'entity_type' => 'media',
+      'bundle' => 'tweet',
+    ])->save();
+
+    $this->entity = Media::create([
+      'bundle' => 'tweet',
+      'tweet' => 'https://twitter.com/foobar/status/12345',
+    ]);
+
+    $this->plugin = new Twitter(
+      MediaBundle::load('tweet')->getTypeConfiguration(),
+      'twitter',
+      [],
+      $this->container->get('entity_type.manager'),
+      $this->container->get('entity_field.manager'),
+      $this->container->get('config.factory'),
+      $this->container->get('renderer'),
+      $this->tweetFetcher
+    );
+
+    $dir = $this->container
+      ->get('config.factory')
+      ->get('media_entity_twitter.settings')
+      ->get('local_images');
+
+    file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+  }
+
+  /**
+   * Tests that an existing local image is used as the thumbnail.
+   */
+  public function testLocalImagePresent() {
+    $this->tweetFetcher
+      ->method('fetchTweet')
+      ->willReturn([
+        'extended_entities' => [
+          'media' => [
+            [
+              'media_url' => 'https://drupal.org/favicon.ico',
+            ],
+          ],
+        ],
+      ]);
+
+    $uri = 'public://twitter-thumbnails/12345.ico';
+    touch($uri);
+    $this->assertEquals($uri, $this->plugin->thumbnail($this->entity));
+  }
+
+  /**
+   * Tests that a local image is downloaded if available but not present.
+   */
+  public function testLocalImageNotPresent() {
+    $this->tweetFetcher
+      ->method('fetchTweet')
+      ->willReturn([
+        'extended_entities' => [
+          'media' => [
+            [
+              'media_url' => 'https://drupal.org/favicon.ico',
+            ],
+          ],
+        ],
+      ]);
+
+    $this->plugin->thumbnail($this->entity);
+    $this->assertFileExists('public://twitter-thumbnails/12345.ico');
+  }
+
+  /**
+   * Tests that the default thumbnail is used if no local image is available.
+   */
+  public function testNoLocalImage() {
+    $this->assertEquals(
+      $this->plugin->getDefaultThumbnail(),
+      $this->plugin->thumbnail($this->entity)
+    );
+  }
+
+  /**
+   * Tests that thumbnail is generated if enabled and local image not available.
+   */
+  public function testThumbnailGeneration() {
+    $configuration = $this->plugin->getConfiguration();
+    $configuration['generate_thumbnails'] = TRUE;
+    $this->plugin->setConfiguration($configuration);
+
+    $uri = $this->plugin->thumbnail($this->entity);
+    $this->assertFileExists($uri);
+  }
+
+}
